| #!/usr/bin/python |
| # Copyright (c) 2008 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| Commandline modification of Xcode project files |
| """ |
| |
| import sys |
| import os |
| import optparse |
| import re |
| import tempfile |
| import random |
| import subprocess |
| |
| random.seed() # Seed the generator |
| |
| |
| # All known project build path source tree path reference types |
| PBX_VALID_SOURCE_TREE_TYPES = ('"<group>"', |
| 'SOURCE_ROOT', |
| '"<absolute>"', |
| 'BUILT_PRODUCTS_DIR', |
| 'DEVELOPER_DIR', |
| 'SDKROOT', |
| 'CONFIGURATION_TEMP_DIR') |
| # Paths with some characters appear quoted |
| QUOTE_PATH_RE = re.compile('\s|-|\+') |
| |
| # Supported Xcode file types |
| EXTENSION_TO_XCODE_FILETYPE = { |
| '.h' : 'sourcecode.c.h', |
| '.c' : 'sourcecode.c.c', |
| '.cpp' : 'sourcecode.cpp.cpp', |
| '.cc' : 'sourcecode.cpp.cpp', |
| '.cxx' : 'sourcecode.cpp.cpp', |
| '.m' : 'sourcecode.c.objc', |
| '.mm' : 'sourcecode.c.objcpp', |
| } |
| |
| # File types that can be added to a Sources phase |
| SOURCES_XCODE_FILETYPES = ( 'sourcecode.c.c', |
| 'sourcecode.cpp.cpp', |
| 'sourcecode.c.objc', |
| 'sourcecode.c.objcpp' ) |
| |
| # Avoid inserting source files into these common Xcode group names. Because |
| # Xcode allows any names for these groups this list cannot be authoritative, |
| # but these are common names in the Xcode templates. |
| NON_SOURCE_GROUP_NAMES = ( 'Frameworks', |
| 'Resources', |
| 'Products', |
| 'Derived Sources', |
| 'Configurations', |
| 'Documentation', |
| 'Frameworks and Libraries', |
| 'External Frameworks and Libraries', |
| 'Libraries' ) |
| |
| |
| def NewUUID(): |
| """Create a new random Xcode UUID""" |
| __pychecker__ = 'unusednames=i' |
| elements = [] |
| for i in range(24): |
| elements.append(hex(random.randint(0, 15))[-1].upper()) |
| return ''.join(elements) |
| |
| def CygwinPathClean(path): |
| """Folks use Cygwin shells with standard Win32 Python which can't handle |
| Cygwin paths. Run everything through cygpath if we can (conveniently |
| cygpath does the right thing with normal Win32 paths). |
| """ |
| # Look for Unix-like path with Win32 Python |
| if sys.platform == 'win32' and path.startswith('/'): |
| cygproc = subprocess.Popen(('cygpath', '-a', '-w', path), |
| stdout=subprocess.PIPE) |
| (stdout_content, stderr_content) = cygproc.communicate() |
| return stdout_content.rstrip() |
| # Convert all paths to cygpaths if we're using cygwin python |
| if sys.platform == 'cygwin': |
| cygproc = subprocess.Popen(('cygpath', '-a', '-u', path), |
| stdout=subprocess.PIPE) |
| (stdout_content, stderr_content) = cygproc.communicate() |
| return stdout_content.rstrip() |
| # Fallthrough for all other cases |
| return path |
| |
| class XcodeProject(object): |
| """Class for reading/writing Xcode project files. |
| This is not a general parser or representation. It is restricted to just |
| the Xcode internal objects we need. |
| |
| Args: |
| path: Absolute path to Xcode project file (including project.pbxproj |
| filename) |
| Attributes: |
| path: Full path to the project.pbxproj file |
| name: Project name (wrapper directory basename without extension) |
| source_root_path: Absolute path for Xcode's SOURCE_ROOT |
| """ |
| |
| EXPECTED_PROJECT_HEADER_RE = re.compile( |
| r'^// !\$\*UTF8\*\$!\n' \ |
| '\{\n' \ |
| '\tarchiveVersion = 1;\n' \ |
| '\tclasses = \{\n' \ |
| '\t\};\n' \ |
| '\tobjectVersion = \d+;\n' \ |
| '\tobjects = \{\n' \ |
| '\n') |
| SECTION_BEGIN_RE = re.compile(r'^/\* Begin (.*) section \*/\n$') |
| SECTION_END_RE = re.compile(r'^/\* End (.*) section \*/\n$') |
| PROJECT_ROOT_OBJECT_RE = re.compile( |
| r'^\trootObject = ([0-9A-F]{24}) /\* Project object \*/;\n$') |
| |
| def __init__(self, path): |
| self.path = path |
| self.name = os.path.splitext(os.path.basename(os.path.dirname(path)))[0] |
| |
| # Load project. Ideally we would use plistlib, but sadly that only reads |
| # XML plists. A real parser with pyparsing |
| # (http://pyparsing.wikispaces.com/) might be another option, but for now |
| # we'll do the simple (?!?) thing. |
| project_fh = open(self.path, 'rU') |
| self._raw_content = project_fh.readlines() |
| project_fh.close() |
| |
| # Store and check header |
| if len(self._raw_content) < 8: |
| print >> sys.stderr, ''.join(self._raw_content) |
| raise RuntimeError('XcodeProject file "%s" too short' % path) |
| self._header = tuple(self._raw_content[:8]) |
| if not self.__class__.EXPECTED_PROJECT_HEADER_RE.match(''.join(self._header)): |
| print >> sys.stderr, ''.join(self._header) |
| raise RuntimeError('XcodeProject file "%s" wrong header' % path) |
| |
| # Find and store tail (some projects have additional whitespace at end) |
| self._tail = [] |
| for tail_line in reversed(self._raw_content): |
| self._tail.insert(0, tail_line) |
| if tail_line == '\t};\n': break |
| |
| # Ugly ugly project parsing, turn each commented section into a separate |
| # set of objects. For types we don't have a custom representation for, |
| # store the raw lines. |
| self._section_order = [] |
| self._sections = {} |
| parse_line_no = len(self._header) |
| while parse_line_no < (len(self._raw_content) - len(self._tail)): |
| section_header_match = self.__class__.SECTION_BEGIN_RE.match( |
| self._raw_content[parse_line_no]) |
| # Loop to next section header |
| if not section_header_match: |
| parse_line_no += 1 |
| continue |
| |
| section = section_header_match.group(1) |
| self._section_order.append(section) |
| self._sections[section] = [] |
| |
| # Advance to first line of the section |
| parse_line_no += 1 |
| |
| # Read in the section, using custom classes where we need them |
| section_end_match = self.__class__.SECTION_END_RE.match( |
| self._raw_content[parse_line_no]) |
| while not section_end_match: |
| # Unhandled lines |
| content = self._raw_content[parse_line_no] |
| # Sections we can parse line-by-line |
| if section in ('PBXBuildFile', 'PBXFileReference'): |
| content = eval('%s.FromContent(content)' % section) |
| # Multiline sections |
| elif section in ('PBXGroup', 'PBXVariantGroup', 'PBXProject', |
| 'PBXNativeTarget', 'PBXSourcesBuildPhase'): |
| # Accumulate lines |
| content_lines = [] |
| while 1: |
| content_lines.append(content) |
| if content == '\t\t};\n': break |
| parse_line_no += 1 |
| content = self._raw_content[parse_line_no] |
| content = eval('%s.FromContent(content_lines)' % section) |
| |
| self._sections[section].append(content) |
| parse_line_no += 1 |
| section_end_match = self.__class__.SECTION_END_RE.match( |
| self._raw_content[parse_line_no]) |
| # Validate section end |
| if section_header_match.group(1) != section: |
| raise RuntimeError( |
| 'XcodeProject parse, section "%s" ended inside section "%s"' % |
| (section_end_match.group(1), section)) |
| # Back around parse loop |
| |
| # Sanity overall group structure |
| if (not self._sections.has_key('PBXProject') or |
| len(self._sections['PBXProject']) != 1): |
| raise RuntimeError('PBXProject section insane') |
| root_obj_parsed = self.__class__.PROJECT_ROOT_OBJECT_RE.match( |
| self._tail[1]) |
| if not root_obj_parsed: |
| raise RuntimeError('XcodeProject unable to parse project root object:\n%s' |
| % self._tail[1]) |
| if root_obj_parsed.group(1) != self._sections['PBXProject'][0].uuid: |
| raise RuntimeError('XcodeProject root object does not match PBXProject') |
| self._root_group_uuid = self._sections['PBXProject'][0].main_group_uuid |
| |
| # Source root |
| self.source_root_path = os.path.abspath( |
| os.path.join( |
| # Directory that contains the project package |
| os.path.dirname(os.path.dirname(path)), |
| # Any relative path |
| self._sections['PBXProject'][0].project_root)) |
| |
| # Build the absolute paths of the groups with these helpers |
| def GroupAbsRealPath(*elements): |
| return os.path.abspath(os.path.realpath(os.path.join(*elements))) |
| def GroupPathRecurse(group, parent_path): |
| descend = False |
| if group.source_tree == '"<absolute>"': |
| group.abs_path = GroupAbsRealPath(group.path) |
| descend = True |
| elif group.source_tree == '"<group>"': |
| if group.path: |
| group.abs_path = GroupAbsRealPath(parent_path, group.path) |
| else: |
| group.abs_path = parent_path |
| descend = True |
| elif group.source_tree == 'SOURCE_ROOT': |
| if group.path: |
| group.abs_path = GroupAbsRealPath(self.source_root_path, group.path) |
| else: |
| group.abs_path = GroupAbsRealPath(self.source_root_path) |
| descend = True |
| if descend: |
| for child_uuid in group.child_uuids: |
| # Try a group first |
| found_uuid = False |
| for other_group in self._sections['PBXGroup']: |
| if other_group.uuid == child_uuid: |
| found_uuid = True |
| GroupPathRecurse(other_group, group.abs_path) |
| break |
| if self._sections.has_key('PBXVariantGroup'): |
| for other_group in self._sections['PBXVariantGroup']: |
| if other_group.uuid == child_uuid: |
| found_uuid = True |
| GroupPathRecurse(other_group, group.abs_path) |
| break |
| if not found_uuid: |
| for file_ref in self._sections['PBXFileReference']: |
| if file_ref.uuid == child_uuid: |
| found_uuid = True |
| if file_ref.source_tree == '"<absolute>"': |
| file_ref.abs_path = GroupAbsRealPath(file_ref.path) |
| elif group.source_tree == '"<group>"': |
| file_ref.abs_path = GroupAbsRealPath(group.abs_path, |
| file_ref.path) |
| elif group.source_tree == 'SOURCE_ROOT': |
| file_ref.abs_path = GroupAbsRealPath(self.source_root_path, |
| file_ref.path) |
| break |
| if not found_uuid: |
| raise RuntimeError('XcodeProject group descent failed to find %s' % |
| child_uuid) |
| self._root_group = None |
| for group in self._sections['PBXGroup']: |
| if group.uuid == self._root_group_uuid: |
| self._root_group = group |
| GroupPathRecurse(group, self.source_root_path) |
| if not self._root_group: |
| raise RuntimeError('XcodeProject failed to find root group by UUID') |
| |
| def FileContent(self): |
| """Generate and return the project file content as a list of lines""" |
| content = [] |
| content.extend(self._header[:-1]) |
| for section in self._section_order: |
| content.append('\n/* Begin %s section */\n' % section) |
| for section_content in self._sections[section]: |
| content.append(str(section_content)) |
| content.append('/* End %s section */\n' % section) |
| content.extend(self._tail) |
| return content |
| |
| def Update(self): |
| """Rewrite the project file in place with all updated metadata""" |
| __pychecker__ = 'no-deprecated' |
| # Not concerned with temp_path security here, just needed a unique name |
| temp_path = tempfile.mktemp(dir=os.path.dirname(self.path)) |
| outfile = open(temp_path, 'w') |
| outfile.writelines(self.FileContent()) |
| outfile.close() |
| # Rename is weird on Win32, see the docs, |
| os.unlink(self.path) |
| os.rename(temp_path, self.path) |
| |
| def NativeTargets(self): |
| """Obtain all PBXNativeTarget instances for this project |
| |
| Returns: |
| List of PBXNativeTarget instances |
| """ |
| if self._sections.has_key('PBXNativeTarget'): |
| return self._sections['PBXNativeTarget'] |
| else: |
| return [] |
| |
| def NativeTargetForName(self, name): |
| """Obtain the target with a given name. |
| |
| Args: |
| name: Target name |
| |
| Returns: |
| PBXNativeTarget instance or None |
| """ |
| for target in self.NativeTargets(): |
| if target.name == name: |
| return target |
| return None |
| |
| def FileReferences(self): |
| """Obtain all PBXFileReference instances for this project |
| |
| Returns: |
| List of PBXFileReference instances |
| """ |
| return self._sections['PBXFileReference'] |
| |
| def SourcesBuildPhaseForTarget(self, target): |
| """Obtain the PBXSourcesBuildPhase instance for a target. Xcode allows |
| only one PBXSourcesBuildPhase per target and each target has a unique |
| PBXSourcesBuildPhase. |
| |
| Args: |
| target: PBXNativeTarget instance |
| |
| Returns: |
| PBXSourcesBuildPhase instance |
| """ |
| sources_uuid = None |
| for i in range(len(target.build_phase_names)): |
| if target.build_phase_names[i] == 'Sources': |
| sources_uuid = target.build_phase_uuids[i] |
| break |
| if not sources_uuid: |
| raise RuntimeError('Missing PBXSourcesBuildPhase for target "%s"' % |
| target.name) |
| for sources_phase in self._sections['PBXSourcesBuildPhase']: |
| if sources_phase.uuid == sources_uuid: |
| return sources_phase |
| raise RuntimeError('Missing PBXSourcesBuildPhase for UUID "%s"' % |
| sources_uuid) |
| |
| def BuildFileForUUID(self, uuid): |
| """Look up a PBXBuildFile by UUID |
| |
| Args: |
| uuid: UUID of the PBXBuildFile to find |
| |
| Raises: |
| RuntimeError if no PBXBuildFile exists for |uuid| |
| |
| Returns: |
| PBXBuildFile instance |
| """ |
| for build_file in self._sections['PBXBuildFile']: |
| if build_file.uuid == uuid: |
| return build_file |
| raise RuntimeError('Missing PBXBuildFile for UUID "%s"' % uuid) |
| |
| def FileReferenceForUUID(self, uuid): |
| """Look up a PBXFileReference by UUID |
| |
| Args: |
| uuid: UUID of the PBXFileReference to find |
| |
| Raises: |
| RuntimeError if no PBXFileReference exists for |uuid| |
| |
| Returns: |
| PBXFileReference instance |
| """ |
| for file_ref in self._sections['PBXFileReference']: |
| if file_ref.uuid == uuid: |
| return file_ref |
| raise RuntimeError('Missing PBXFileReference for UUID "%s"' % uuid) |
| |
| def RemoveSourceFileReference(self, file_ref): |
| """Remove a source file's PBXFileReference from the project, cleaning up all |
| PBXGroup and PBXBuildFile references to that PBXFileReference and |
| furthermore, removing any PBXBuildFiles from all PBXNativeTarget source |
| lists. |
| |
| Args: |
| file_ref: PBXFileReference instance |
| |
| Raises: |
| RuntimeError if |file_ref| is not a source file reference in PBXBuildFile |
| """ |
| self._sections['PBXFileReference'].remove(file_ref) |
| # Clean up build files |
| removed_build_files = [] |
| for build_file in self._sections['PBXBuildFile']: |
| if build_file.file_ref_uuid == file_ref.uuid: |
| if build_file.type != 'Sources': |
| raise RuntimeError('Removing PBXBuildFile not of "Sources" type') |
| removed_build_files.append(build_file) |
| removed_build_file_uuids = [] |
| for build_file in removed_build_files: |
| removed_build_file_uuids.append(build_file.uuid) |
| self._sections['PBXBuildFile'].remove(build_file) |
| # Clean up source references to the removed build files |
| for source_phase in self._sections['PBXSourcesBuildPhase']: |
| removal_indexes = [] |
| for i in range(len(source_phase.file_uuids)): |
| if source_phase.file_uuids[i] in removed_build_file_uuids: |
| removal_indexes.append(i) |
| for removal_index in removal_indexes: |
| del source_phase.file_uuids[removal_index] |
| del source_phase.file_names[removal_index] |
| # Clean up group references |
| for group in self._sections['PBXGroup']: |
| removal_indexes = [] |
| for i in range(len(group.child_uuids)): |
| if group.child_uuids[i] == file_ref.uuid: |
| removal_indexes.append(i) |
| for removal_index in removal_indexes: |
| del group.child_uuids[removal_index] |
| del group.child_names[removal_index] |
| |
| def RelativeSourceRootPath(self, abs_path): |
| """Convert a path to one relative to the project's SOURCE_ROOT if possible. |
| Generally this follows Xcode semantics, that is, a path is only converted |
| if it is a subpath of SOURCE_ROOT. |
| |
| Args: |
| abs_path: Absolute path to convert |
| |
| Returns: |
| String SOURCE_ROOT relative path if possible or None if not relative |
| to SOURCE_ROOT. |
| """ |
| if abs_path.startswith(self.source_root_path + os.path.sep): |
| return abs_path[len(self.source_root_path + os.path.sep):] |
| else: |
| # Try to construct a relative path (bodged from ActiveState recipe |
| # 302594 since we can't assume Python 2.5 with os.path.relpath() |
| source_root_parts = self.source_root_path.split(os.path.sep) |
| target_parts = abs_path.split(os.path.sep) |
| # Guard against drive changes on Win32 and cygwin |
| if sys.platform == 'win32' and source_root_parts[0] <> target_parts[0]: |
| return None |
| if sys.platform == 'cygwin' and source_root_parts[2] <> target_parts[2]: |
| return None |
| for i in range(min(len(source_root_parts), len(target_parts))): |
| if source_root_parts[i] <> target_parts[i]: break |
| else: |
| i += 1 |
| rel_parts = [os.path.pardir] * (len(source_root_parts) - i) |
| rel_parts.extend(target_parts[i:]) |
| return os.path.join(*rel_parts) |
| |
| def RelativeGroupPath(self, abs_path): |
| """Convert a path to a group-relative path if possible |
| |
| Args: |
| abs_path: Absolute path to convert |
| |
| Returns: |
| Parent PBXGroup instance if possible or None |
| """ |
| needed_path = os.path.dirname(abs_path) |
| possible_groups = [ g for g in self._sections['PBXGroup'] |
| if g.abs_path == needed_path and |
| not g.name in NON_SOURCE_GROUP_NAMES ] |
| if len(possible_groups) < 1: |
| return None |
| elif len(possible_groups) == 1: |
| return possible_groups[0] |
| # Multiple groups match, try to find the best using some simple |
| # heuristics. Does only one group contain source? |
| groups_with_source = [] |
| for group in possible_groups: |
| for child_uuid in group.child_uuids: |
| try: |
| self.FileReferenceForUUID(child_uuid) |
| except RuntimeError: |
| pass |
| else: |
| groups_with_source.append(group) |
| break |
| if len(groups_with_source) == 1: |
| return groups_with_source[0] |
| # Is only one _not_ the root group? |
| non_root_groups = [ g for g in possible_groups |
| if g is not self._root_group ] |
| if len(non_root_groups) == 1: |
| return non_root_groups[0] |
| # Best guess |
| if len(non_root_groups): |
| return non_root_groups[0] |
| elif len(groups_with_source): |
| return groups_with_source[0] |
| else: |
| return possible_groups[0] |
| |
| def AddSourceFile(self, path): |
| """Add a source file to the project, attempting to position it |
| in the GUI group heirarchy reasonably. |
| |
| NOTE: Adding a source file does not add it to any targets |
| |
| Args: |
| path: Absolute path to the file to add |
| |
| Returns: |
| PBXFileReference instance for the newly added source. |
| """ |
| # Guess at file type |
| root, extension = os.path.splitext(path) |
| if EXTENSION_TO_XCODE_FILETYPE.has_key(extension): |
| source_type = EXTENSION_TO_XCODE_FILETYPE[extension] |
| else: |
| raise RuntimeError('Unknown source file extension "%s"' % extension) |
| |
| # Is group-relative possible for an existing group? |
| parent_group = self.RelativeGroupPath(os.path.abspath(path)) |
| if parent_group: |
| new_file_ref = PBXFileReference(NewUUID(), |
| os.path.basename(path), |
| source_type, |
| None, |
| os.path.basename(path), |
| '"<group>"', |
| None) |
| # Chrome tries to keep its lists name sorted, try to match |
| i = 0 |
| while i < len(parent_group.child_uuids): |
| # Only files are sorted, they keep groups at the top |
| try: |
| self.FileReferenceForUUID(parent_group.child_uuids[i]) |
| if new_file_ref.name.lower() < parent_group.child_names[i].lower(): |
| break |
| except RuntimeError: |
| pass # Must be a child group |
| i += 1 |
| parent_group.child_names.insert(i, new_file_ref.name) |
| parent_group.child_uuids.insert(i, new_file_ref.uuid) |
| # Add file ref uuid sorted |
| self._sections['PBXFileReference'].append(new_file_ref) |
| self._sections['PBXFileReference'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid)) |
| return new_file_ref |
| |
| # Group-relative failed, how about SOURCE_ROOT relative in the main group |
| src_rel_path = self.RelativeSourceRootPath(os.path.abspath(path)) |
| if src_rel_path: |
| src_rel_path = src_rel_path.replace('\\', '/') # Convert to Unix |
| new_file_ref = PBXFileReference(NewUUID(), |
| os.path.basename(path), |
| source_type, |
| None, |
| src_rel_path, |
| 'SOURCE_ROOT', |
| None) |
| self._root_group.child_uuids.append(new_file_ref.uuid) |
| self._root_group.child_names.append(new_file_ref.name) |
| # Add file ref uuid sorted |
| self._sections['PBXFileReference'].append(new_file_ref) |
| self._sections['PBXFileReference'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid)) |
| return new_file_ref |
| |
| # Win to Unix absolute paths probably not practical |
| raise RuntimeError('Could not construct group or source PBXFileReference ' |
| 'for path "%s"' % path) |
| |
| def AddSourceFileToSourcesBuildPhase(self, source_ref, source_phase): |
| """Add a PBXFileReference to a PBXSourcesBuildPhase, creating a new |
| PBXBuildFile as needed. |
| |
| Args: |
| source_ref: PBXFileReference instance appropriate for use in |
| PBXSourcesBuildPhase |
| source_phase: PBXSourcesBuildPhase instance |
| """ |
| # Prevent duplication |
| for source_uuid in source_phase.file_uuids: |
| build_file = self.BuildFileForUUID(source_uuid) |
| if build_file.file_ref_uuid == source_ref.uuid: |
| return |
| # Create PBXBuildFile |
| new_build_file = PBXBuildFile(NewUUID(), |
| source_ref.name, |
| 'Sources', |
| source_ref.uuid, |
| '') |
| # Add to build file list (uuid sorted) |
| self._sections['PBXBuildFile'].append(new_build_file) |
| self._sections['PBXBuildFile'].sort(cmp=lambda x,y: cmp(x.uuid, y.uuid)) |
| # Add to sources phase list (name sorted) |
| i = 0 |
| while i < len(source_phase.file_names): |
| if source_ref.name.lower() < source_phase.file_names[i].lower(): |
| break |
| i += 1 |
| source_phase.file_names.insert(i, new_build_file.name) |
| source_phase.file_uuids.insert(i, new_build_file.uuid) |
| |
| |
| class PBXProject(object): |
| """Class for PBXProject data section of an Xcode project file. |
| |
| Attributes: |
| uuid: Project UUID |
| main_group_uuid: UUID of the top-level PBXGroup |
| project_root: Relative path from project file wrapper to source_root_path |
| """ |
| |
| PBXPROJECT_HEADER_RE = re.compile( |
| r'^\t\t([0-9A-F]{24}) /\* Project object \*/ = {\n$') |
| PBXPROJECT_MAIN_GROUP_RE = re.compile( |
| r'^\t\t\tmainGroup = ([0-9A-F]{24})(?: /\* .* \*/)?;\n$') |
| PBXPROJECT_ROOT_RE = re.compile( |
| r'^\t\t\tprojectRoot = (.*);\n$') |
| |
| @classmethod |
| def FromContent(klass, content_lines): |
| header_parsed = klass.PBXPROJECT_HEADER_RE.match(content_lines[0]) |
| if not header_parsed: |
| raise RuntimeError('PBXProject unable to parse header content:\n%s' |
| % content_lines[0]) |
| main_group_uuid = None |
| project_root = '' |
| for content_line in content_lines: |
| group_parsed = klass.PBXPROJECT_MAIN_GROUP_RE.match(content_line) |
| if group_parsed: |
| main_group_uuid = group_parsed.group(1) |
| root_parsed = klass.PBXPROJECT_ROOT_RE.match(content_line) |
| if root_parsed: |
| project_root = root_parsed.group(1) |
| if project_root.startswith('"'): |
| project_root = project_root[1:-1] |
| if not main_group_uuid: |
| raise RuntimeError('PBXProject missing main group') |
| return klass(content_lines, header_parsed.group(1), |
| main_group_uuid, project_root) |
| |
| def __init__(self, raw_lines, uuid, main_group_uuid, project_root): |
| self.uuid = uuid |
| self._raw_lines = raw_lines |
| self.main_group_uuid = main_group_uuid |
| self.project_root = project_root |
| |
| def __str__(self): |
| return ''.join(self._raw_lines) |
| |
| |
| class PBXBuildFile(object): |
| """Class for PBXBuildFile data from an Xcode project file. |
| |
| Attributes: |
| uuid: UUID for this instance |
| name: Basename of the build file |
| type: 'Sources' or 'Frameworks' |
| file_ref_uuid: UUID of the PBXFileReference for this file |
| """ |
| |
| PBXBUILDFILE_LINE_RE = re.compile( |
| r'^\t\t([0-9A-F]{24}) /\* (.+) in (.+) \*/ = ' |
| '{isa = PBXBuildFile; fileRef = ([0-9A-F]{24}) /\* (.+) \*/; (.*)};\n$') |
| |
| @classmethod |
| def FromContent(klass, content_line): |
| parsed = klass.PBXBUILDFILE_LINE_RE.match(content_line) |
| if not parsed: |
| raise RuntimeError('PBXBuildFile unable to parse content:\n%s' |
| % content_line) |
| if parsed.group(2) != parsed.group(5): |
| raise RuntimeError('PBXBuildFile name mismatch "%s" vs "%s"' % |
| (parsed.group(2), parsed.group(5))) |
| if not parsed.group(3) in ('Sources', 'Frameworks', |
| 'Resources', 'CopyFiles', |
| 'Headers', 'Copy Into Framework', |
| 'Rez', 'Copy Generated Headers'): |
| raise RuntimeError('PBXBuildFile unknown type "%s"' % parsed.group(3)) |
| return klass(parsed.group(1), parsed.group(2), parsed.group(3), |
| parsed.group(4), parsed.group(6)) |
| |
| def __init__(self, uuid, name, type, file_ref_uuid, raw_extras): |
| self.uuid = uuid |
| self.name = name |
| self.type = type |
| self.file_ref_uuid = file_ref_uuid |
| self._raw_extras = raw_extras |
| |
| def __str__(self): |
| return '\t\t%s /* %s in %s */ = ' \ |
| '{isa = PBXBuildFile; fileRef = %s /* %s */; %s};\n' % ( |
| self.uuid, self.name, self.type, self.file_ref_uuid, self.name, |
| self._raw_extras) |
| |
| |
| class PBXFileReference(object): |
| """Class for PBXFileReference data from an Xcode project file. |
| |
| Attributes: |
| uuid: UUID for this instance |
| name: Basename of the file |
| file_type: current active file type (explicit or assumed) |
| path: source_tree relative path (or absolute if source_tree is absolute) |
| source_tree: Source tree type (see PBX_VALID_SOURCE_TREE_TYPES) |
| abs_path: Absolute path to the file |
| """ |
| PBXFILEREFERENCE_HEADER_RE = re.compile( |
| r'^\t\t([0-9A-F]{24}) /\* (.+) \*/ = {isa = PBXFileReference; ') |
| PBXFILEREFERENCE_FILETYPE_RE = re.compile( |
| r' (lastKnownFileType|explicitFileType) = ([^\;]+); ') |
| PBXFILEREFERENCE_PATH_RE = re.compile(r' path = ([^\;]+); ') |
| PBXFILEREFERENCE_SOURCETREE_RE = re.compile(r' sourceTree = ([^\;]+); ') |
| |
| @classmethod |
| def FromContent(klass, content_line): |
| header_parsed = klass.PBXFILEREFERENCE_HEADER_RE.match(content_line) |
| if not header_parsed: |
| raise RuntimeError('PBXFileReference unable to parse header content:\n%s' |
| % content_line) |
| type_parsed = klass.PBXFILEREFERENCE_FILETYPE_RE.search(content_line) |
| if not type_parsed: |
| raise RuntimeError('PBXFileReference unable to parse type content:\n%s' |
| % content_line) |
| if type_parsed.group(1) == 'lastKnownFileType': |
| last_known_type = type_parsed.group(2) |
| explicit_type = None |
| else: |
| last_known_type = None |
| explicit_type = type_parsed.group(2) |
| path_parsed = klass.PBXFILEREFERENCE_PATH_RE.search(content_line) |
| if not path_parsed: |
| raise RuntimeError('PBXFileReference unable to parse path content:\n%s' |
| % content_line) |
| tree_parsed = klass.PBXFILEREFERENCE_SOURCETREE_RE.search(content_line) |
| if not tree_parsed: |
| raise RuntimeError( |
| 'PBXFileReference unable to parse source tree content:\n%s' |
| % content_line) |
| return klass(header_parsed.group(1), header_parsed.group(2), |
| last_known_type, explicit_type, path_parsed.group(1), |
| tree_parsed.group(1), content_line) |
| |
| def __init__(self, uuid, name, last_known_file_type, explicit_file_type, |
| path, source_tree, raw_line): |
| self.uuid = uuid |
| self.name = name |
| self._last_known_file_type = last_known_file_type |
| self._explicit_file_type = explicit_file_type |
| if explicit_file_type: |
| self.file_type = explicit_file_type |
| else: |
| self.file_type = last_known_file_type |
| self.path = path |
| self.source_tree = source_tree |
| self.abs_path = None |
| self._raw_line = raw_line |
| |
| def __str__(self): |
| # Raw available? |
| if self._raw_line: return self._raw_line |
| # Construct our own |
| if self._last_known_file_type: |
| print_file_type = 'lastKnownFileType = %s; ' % self._last_known_file_type |
| elif self._explicit_file_type: |
| print_file_type = 'explicitFileType = %s; ' % self._explicit_file_type |
| else: |
| raise RuntimeError('No known file type for stringification') |
| name_attribute = '' |
| if self.name != self.path: |
| name_attribute = 'name = %s; ' % self.name |
| print_path = self.path |
| if QUOTE_PATH_RE.search(print_path): |
| print_path = '"%s"' % print_path |
| return '\t\t%s /* %s */ = ' \ |
| '{isa = PBXFileReference; ' \ |
| 'fileEncoding = 4; ' \ |
| '%s' \ |
| '%s' \ |
| 'path = %s; sourceTree = %s; };\n' % ( |
| self.uuid, self.name, print_file_type, |
| name_attribute, print_path, self.source_tree) |
| |
| |
| class PBXGroup(object): |
| """Class for PBXGroup data from an Xcode project file. |
| |
| Attributes: |
| uuid: UUID for this instance |
| name: Group (folder) name |
| path: source_tree relative path (or absolute if source_tree is absolute) |
| source_tree: Source tree type (see PBX_VALID_SOURCE_TREE_TYPES) |
| abs_path: Absolute path to the group |
| child_uuids: Ordered list of PBXFileReference UUIDs |
| child_names: Ordered list of PBXFileReference names |
| """ |
| |
| PBXGROUP_HEADER_RE = re.compile(r'^\t\t([0-9A-F]{24}) (?:/\* .* \*/ )?= {\n$') |
| PBXGROUP_FIELD_RE = re.compile(r'^\t\t\t(.*) = (.*);\n$') |
| PBXGROUP_CHILD_RE = re.compile(r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) \*/,\n$') |
| |
| @classmethod |
| def FromContent(klass, content_lines): |
| # Header line |
| header_parsed = klass.PBXGROUP_HEADER_RE.match(content_lines[0]) |
| if not header_parsed: |
| raise RuntimeError('PBXGroup unable to parse header content:\n%s' |
| % content_lines[0]) |
| name = None |
| path = '' |
| source_tree = None |
| tab_width = None |
| uses_tabs = None |
| indent_width = None |
| child_uuids = [] |
| child_names = [] |
| # Parse line by line |
| content_line_no = 0 |
| while 1: |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| if content_line == '\t\t};\n': break |
| if content_line == '\t\t\tisa = PBXGroup;\n': continue |
| if content_line == '\t\t\tisa = PBXVariantGroup;\n': continue |
| # Child groups |
| if content_line == '\t\t\tchildren = (\n': |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| while content_line != '\t\t\t);\n': |
| child_parsed = klass.PBXGROUP_CHILD_RE.match(content_line) |
| if not child_parsed: |
| raise RuntimeError('PBXGroup unable to parse child content:\n%s' |
| % content_line) |
| child_uuids.append(child_parsed.group(1)) |
| child_names.append(child_parsed.group(2)) |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| continue # Back to top of loop on end of children |
| # Other fields |
| field_parsed = klass.PBXGROUP_FIELD_RE.match(content_line) |
| if not field_parsed: |
| raise RuntimeError('PBXGroup unable to parse field content:\n%s' |
| % content_line) |
| if field_parsed.group(1) == 'name': |
| name = field_parsed.group(2) |
| elif field_parsed.group(1) == 'path': |
| path = field_parsed.group(2) |
| elif field_parsed.group(1) == 'sourceTree': |
| if not field_parsed.group(2) in PBX_VALID_SOURCE_TREE_TYPES: |
| raise RuntimeError('PBXGroup unknown source tree type "%s"' |
| % field_parsed.group(2)) |
| source_tree = field_parsed.group(2) |
| elif field_parsed.group(1) == 'tabWidth': |
| tab_width = field_parsed.group(2) |
| elif field_parsed.group(1) == 'usesTabs': |
| uses_tabs = field_parsed.group(2) |
| elif field_parsed.group(1) == 'indentWidth': |
| indent_width = field_parsed.group(2) |
| else: |
| raise RuntimeError('PBXGroup unknown field "%s"' |
| % field_parsed.group(1)) |
| if path and path.startswith('"'): |
| path = path[1:-1] |
| if name and name.startswith('"'): |
| name = name[1:-1] |
| return klass(header_parsed.group(1), name, path, source_tree, child_uuids, |
| child_names, tab_width, uses_tabs, indent_width) |
| |
| def __init__(self, uuid, name, path, source_tree, child_uuids, child_names, |
| tab_width, uses_tabs, indent_width): |
| self.uuid = uuid |
| self.name = name |
| self.path = path |
| self.source_tree = source_tree |
| self.child_uuids = child_uuids |
| self.child_names = child_names |
| self.abs_path = None |
| # Semantically I'm not sure these aren't an error, but they |
| # appear in some projects |
| self._tab_width = tab_width |
| self._uses_tabs = uses_tabs |
| self._indent_width = indent_width |
| |
| def __str__(self): |
| if self.name: |
| header_comment = '/* %s */ ' % self.name |
| elif self.path: |
| header_comment = '/* %s */ ' % self.path |
| else: |
| header_comment = '' |
| if self.name: |
| if QUOTE_PATH_RE.search(self.name): |
| name_attribute = '\t\t\tname = "%s";\n' % self.name |
| else: |
| name_attribute = '\t\t\tname = %s;\n' % self.name |
| else: |
| name_attribute = '' |
| if self.path: |
| if QUOTE_PATH_RE.search(self.path): |
| path_attribute = '\t\t\tpath = "%s";\n' % self.path |
| else: |
| path_attribute = '\t\t\tpath = %s;\n' % self.path |
| else: |
| path_attribute = '' |
| child_lines = [] |
| for x in range(len(self.child_uuids)): |
| child_lines.append('\t\t\t\t%s /* %s */,\n' % |
| (self.child_uuids[x], self.child_names[x])) |
| children = ''.join(child_lines) |
| tab_width_attribute = '' |
| if self._tab_width: |
| tab_width_attribute = '\t\t\ttabWidth = %s;\n' % self._tab_width |
| uses_tabs_attribute = '' |
| if self._uses_tabs: |
| uses_tabs_attribute = '\t\t\tusesTabs = %s;\n' % self._uses_tabs |
| indent_width_attribute = '' |
| if self._indent_width: |
| indent_width_attribute = '\t\t\tindentWidth = %s;\n' % self._indent_width |
| return '\t\t%s %s= {\n' \ |
| '\t\t\tisa = %s;\n' \ |
| '\t\t\tchildren = (\n' \ |
| '%s' \ |
| '\t\t\t);\n' \ |
| '%s' \ |
| '%s' \ |
| '%s' \ |
| '\t\t\tsourceTree = %s;\n' \ |
| '%s' \ |
| '%s' \ |
| '\t\t};\n' % ( |
| self.uuid, header_comment, |
| self.__class__.__name__, |
| children, |
| indent_width_attribute, |
| name_attribute, |
| path_attribute, self.source_tree, |
| tab_width_attribute, uses_tabs_attribute) |
| |
| |
| class PBXVariantGroup(PBXGroup): |
| pass |
| |
| |
| class PBXNativeTarget(object): |
| """Class for PBXNativeTarget data from an Xcode project file. |
| |
| Attributes: |
| name: Target name |
| build_phase_uuids: Ordered list of build phase UUIDs |
| build_phase_names: Ordered list of build phase names |
| |
| NOTE: We do not have wrapper classes for all build phase data types! |
| """ |
| |
| PBXNATIVETARGET_HEADER_RE = re.compile( |
| r'^\t\t([0-9A-F]{24}) /\* (.*) \*/ = {\n$') |
| PBXNATIVETARGET_BUILD_PHASE_RE = re.compile( |
| r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) \*/,\n$') |
| |
| @classmethod |
| def FromContent(klass, content_lines): |
| header_parsed = klass.PBXNATIVETARGET_HEADER_RE.match(content_lines[0]) |
| if not header_parsed: |
| raise RuntimeError('PBXNativeTarget unable to parse header content:\n%s' |
| % content_lines[0]) |
| build_phase_uuids = [] |
| build_phase_names = [] |
| content_line_no = 0 |
| while 1: |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| if content_line == '\t\t};\n': break |
| if content_line == '\t\t\tisa = PBXNativeTarget;\n': continue |
| # Build phases groups |
| if content_line == '\t\t\tbuildPhases = (\n': |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| while content_line != '\t\t\t);\n': |
| phase_parsed = klass.PBXNATIVETARGET_BUILD_PHASE_RE.match( |
| content_line) |
| if not phase_parsed: |
| raise RuntimeError( |
| 'PBXNativeTarget unable to parse build phase content:\n%s' |
| % content_line) |
| build_phase_uuids.append(phase_parsed.group(1)) |
| build_phase_names.append(phase_parsed.group(2)) |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| break # Don't care about the rest of the content |
| return klass(content_lines, header_parsed.group(2), build_phase_uuids, |
| build_phase_names) |
| |
| def __init__(self, raw_lines, name, build_phase_uuids, build_phase_names): |
| self._raw_lines = raw_lines |
| self.name = name |
| self.build_phase_uuids = build_phase_uuids |
| self.build_phase_names = build_phase_names |
| |
| def __str__(self): |
| return ''.join(self._raw_lines) |
| |
| |
| class PBXSourcesBuildPhase(object): |
| """Class for PBXSourcesBuildPhase data from an Xcode project file. |
| |
| Attributes: |
| uuid: UUID for this instance |
| build_action_mask: Xcode magic mask constant |
| run_only_for_deployment_postprocessing: deployment postprocess flag |
| file_uuids: Ordered list of PBXBuildFile UUIDs |
| file_names: Ordered list of PBXBuildFile names (basename) |
| """ |
| |
| PBXSOURCESBUILDPHASE_HEADER_RE = re.compile( |
| r'^\t\t([0-9A-F]{24}) /\* Sources \*/ = {\n$') |
| PBXSOURCESBUILDPHASE_FIELD_RE = re.compile(r'^\t\t\t(.*) = (.*);\n$') |
| PBXSOURCESBUILDPHASE_FILE_RE = re.compile( |
| r'^\t\t\t\t([0-9A-F]{24}) /\* (.*) in Sources \*/,\n$') |
| |
| @classmethod |
| def FromContent(klass, content_lines): |
| header_parsed = klass.PBXSOURCESBUILDPHASE_HEADER_RE.match(content_lines[0]) |
| if not header_parsed: |
| raise RuntimeError( |
| 'PBXSourcesBuildPhase unable to parse header content:\n%s' |
| % content_lines[0]) |
| # Parse line by line |
| build_action_mask = None |
| run_only_for_deployment_postprocessing = None |
| file_uuids = [] |
| file_names = [] |
| content_line_no = 0 |
| while 1: |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| if content_line == '\t\t};\n': break |
| if content_line == '\t\t\tisa = PBXSourcesBuildPhase;\n': continue |
| # Files |
| if content_line == '\t\t\tfiles = (\n': |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| while content_line != '\t\t\t);\n': |
| file_parsed = klass.PBXSOURCESBUILDPHASE_FILE_RE.match(content_line) |
| if not file_parsed: |
| raise RuntimeError( |
| 'PBXSourcesBuildPhase unable to parse file content:\n%s' |
| % content_line) |
| file_uuids.append(file_parsed.group(1)) |
| file_names.append(file_parsed.group(2)) |
| content_line_no += 1 |
| content_line = content_lines[content_line_no] |
| continue # Back to top of loop on end of files list |
| # Other fields |
| field_parsed = klass.PBXSOURCESBUILDPHASE_FIELD_RE.match(content_line) |
| if not field_parsed: |
| raise RuntimeError( |
| 'PBXSourcesBuildPhase unable to parse field content:\n%s' |
| % content_line) |
| if field_parsed.group(1) == 'buildActionMask': |
| build_action_mask = field_parsed.group(2) |
| elif field_parsed.group(1) == 'runOnlyForDeploymentPostprocessing': |
| run_only_for_deployment_postprocessing = field_parsed.group(2) |
| else: |
| raise RuntimeError('PBXSourcesBuildPhase unknown field "%s"' |
| % field_parsed.group(1)) |
| return klass(header_parsed.group(1), build_action_mask, |
| run_only_for_deployment_postprocessing, |
| file_uuids, file_names) |
| |
| def __init__(self, uuid, build_action_mask, |
| run_only_for_deployment_postprocessing, |
| file_uuids, file_names): |
| self.uuid = uuid |
| self.build_action_mask = build_action_mask |
| self.run_only_for_deployment_postprocessing = \ |
| run_only_for_deployment_postprocessing |
| self.file_uuids = file_uuids |
| self.file_names = file_names |
| |
| def __str__(self): |
| file_lines = [] |
| for x in range(len(self.file_uuids)): |
| file_lines.append('\t\t\t\t%s /* %s in Sources */,\n' % |
| (self.file_uuids[x], self.file_names[x])) |
| files = ''.join(file_lines) |
| return '\t\t%s /* Sources */ = {\n' \ |
| '\t\t\tisa = PBXSourcesBuildPhase;\n' \ |
| '\t\t\tbuildActionMask = %s;\n' \ |
| '\t\t\tfiles = (\n' \ |
| '%s' \ |
| '\t\t\t);\n' \ |
| '\t\t\trunOnlyForDeploymentPostprocessing = %s;\n' \ |
| '\t\t};\n' % ( |
| self.uuid, self.build_action_mask, files, |
| self.run_only_for_deployment_postprocessing) |
| |
| |
| def Usage(optparse): |
| optparse.print_help() |
| print '\n' \ |
| 'Commands:\n' \ |
| ' list_native_targets: List Xcode "native" (source compilation)\n' \ |
| ' targets by name.\n' \ |
| ' list_target_sources: List project-relative source files in the\n' \ |
| ' specified Xcode "native" target.\n' \ |
| ' remove_source [sourcefile ...]: Remove the specified source files\n' \ |
| ' from every target in the project (target is ignored).\n' \ |
| ' add_source [sourcefile ...]: Add the specified source files\n' \ |
| ' to the specified target.\n' |
| sys.exit(2) |
| |
| |
| def Main(): |
| # Use argument structure like xcodebuild commandline |
| option_parser = optparse.OptionParser( |
| usage='usage: %prog -p projectname [ -t targetname ] ' \ |
| '<command> [...]', |
| add_help_option=False) |
| option_parser.add_option( |
| '-h', '--help', action='store_true', dest='help', |
| default=False, help=optparse.SUPPRESS_HELP) |
| option_parser.add_option( |
| '-p', '--project', action='store', type='string', |
| dest='project', metavar='projectname', |
| help='Manipulate the project specified by projectname.') |
| option_parser.add_option( |
| '-t', '--target', action='store', type='string', |
| dest='target', metavar='targetname', |
| help='Manipulate the target specified by targetname.') |
| (options, args) = option_parser.parse_args() |
| |
| # Since we have more elaborate commands, handle help |
| if options.help: |
| Usage(option_parser) |
| |
| # Xcode project file |
| if not options.project: |
| option_parser.error('Xcode project file must be specified.') |
| project_path = os.path.abspath(CygwinPathClean(options.project)) |
| if project_path.endswith('.xcodeproj'): |
| project_path = os.path.join(project_path, 'project.pbxproj') |
| if not project_path.endswith(os.sep + 'project.pbxproj'): |
| option_parser.error('Invalid Xcode project file path \"%s\"' % project_path) |
| if not os.path.exists(project_path): |
| option_parser.error('Missing Xcode project file \"%s\"' % project_path) |
| |
| # Construct project object |
| project = XcodeProject(project_path) |
| |
| # Switch on command |
| |
| # List native target names (default command) |
| if len(args) < 1 or args[0] == 'list_native_targets': |
| # Ape xcodebuild output |
| target_names = [] |
| for target in project.NativeTargets(): |
| target_names.append(target.name) |
| print 'Information about project "%s"\n Native Targets:\n %s' % ( |
| project.name, |
| '\n '.join(target_names)) |
| |
| if len(args) < 1: |
| # Be friendly and print some hints for further actions. |
| print |
| print 'To add or remove files from given target, run:' |
| print '\txcodebodge.py -p <project> -t <target> add_source <file_name>' |
| print '\txcodebodge.py -p <project> -t <target> remove_source <file_name>' |
| |
| # List files in a native target |
| elif args[0] == 'list_target_sources': |
| if len(args) != 1: |
| option_parser.error('list_target_sources takes no arguments') |
| if not options.target: |
| option_parser.error('list_target_sources requires a target') |
| # Validate target and get list of files |
| target = project.NativeTargetForName(options.target) |
| if not target: |
| option_parser.error('No native target named "%s"' % options.target) |
| sources_phase = project.SourcesBuildPhaseForTarget(target) |
| target_files = [] |
| for source_uuid in sources_phase.file_uuids: |
| build_file = project.BuildFileForUUID(source_uuid) |
| file_ref = project.FileReferenceForUUID(build_file.file_ref_uuid) |
| pretty_path = project.RelativeSourceRootPath(file_ref.abs_path) |
| if pretty_path: |
| target_files.append(pretty_path) |
| else: |
| target_files.append(file_ref.abs_path) |
| # Ape xcodebuild output |
| print 'Information about project "%s" target "%s"\n' \ |
| ' Files:\n %s' % (project.name, options.target, |
| '\n '.join(target_files)) |
| |
| # Remove source files |
| elif args[0] == 'remove_source': |
| if len(args) < 2: |
| option_parser.error('remove_source needs one or more source files') |
| if options.target: |
| option_parser.error( |
| 'remove_source does not support removal from a single target') |
| for source_path in args[1:]: |
| source_path = CygwinPathClean(source_path) |
| found = False |
| for file_ref in project.FileReferences(): |
| # Try undecorated path, abs_path and our prettified paths |
| if (file_ref.path == source_path or ( |
| file_ref.abs_path and ( |
| file_ref.abs_path == os.path.abspath(source_path) or |
| project.RelativeSourceRootPath(file_ref.abs_path) == source_path))): |
| # Found a matching file ref, remove it |
| found = True |
| project.RemoveSourceFileReference(file_ref) |
| if not found: |
| option_parser.error('No matching source file "%s"' % source_path) |
| project.Update() |
| |
| # Add source files |
| elif args[0] == 'add_source': |
| if len(args) < 2: |
| option_parser.error('add_source needs one or more source files') |
| if not options.target: |
| option_parser.error('add_source requires a target') |
| # Look for the target we want to add too. |
| target = project.NativeTargetForName(options.target) |
| if not target: |
| option_parser.error('No native target named "%s"' % options.target) |
| # Get the sources build phase |
| sources_phase = project.SourcesBuildPhaseForTarget(target) |
| # Loop new sources |
| for source_path in args[1:]: |
| source_path = CygwinPathClean(source_path) |
| if not os.path.exists(os.path.abspath(source_path)): |
| option_parser.error('File "%s" not found' % source_path) |
| # Don't generate duplicate file references if we don't need them |
| source_ref = None |
| for file_ref in project.FileReferences(): |
| # Try undecorated path, abs_path and our prettified paths |
| if (file_ref.path == source_path or ( |
| file_ref.abs_path and ( |
| file_ref.abs_path == os.path.abspath(source_path) or |
| project.RelativeSourceRootPath(file_ref.abs_path) == source_path))): |
| source_ref = file_ref |
| break |
| if not source_ref: |
| # Create a new source file ref |
| source_ref = project.AddSourceFile(os.path.abspath(source_path)) |
| # Add the new source file reference to the target if its a safe type |
| if source_ref.file_type in SOURCES_XCODE_FILETYPES: |
| project.AddSourceFileToSourcesBuildPhase(source_ref, sources_phase) |
| project.Update() |
| |
| # Private sanity check. On an unmodified project make sure our output is |
| # the same as the input |
| elif args[0] == 'parse_sanity': |
| if ''.join(project.FileContent()) != ''.join(project._raw_content): |
| option_parser.error('Project rewrite sanity fail "%s"' % project.path) |
| |
| else: |
| Usage(option_parser) |
| |
| |
| if __name__ == '__main__': |
| Main() |