├── LICENSE ├── MANIFEST.in ├── PKG-INFO ├── README.rst ├── examples ├── add_buildphase.py ├── add_buildphase.rb ├── emojis.txt ├── examine_local_projects.py ├── firstnames.txt ├── gidhistograms.py └── utils.py ├── setup.cfg ├── setup.py ├── tests ├── check_xcode_behaviour.py ├── data │ ├── IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj │ │ ├── project.json │ │ ├── project.pbxproj │ │ └── project.xml │ └── MiniProject │ │ └── MiniProject.xcodeproj │ │ └── project.pbxproj └── test_xcodeprojer.py ├── tox.ini └── xcodeprojer.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Krause ( http://krause-software.com ) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include xcodeprojer.py tox.ini README.rst MANIFEST.in LICENSE 2 | recursive-exclude . 3 | recursive-include tests *.py 4 | recursive-include tests *.pbxproj 5 | recursive-include tests *.json 6 | recursive-include tests *.xml 7 | recursive-exclude tests *.pyc 8 | recursive-exclude tests *.pyo 9 | recursive-exclude tests/output * 10 | recursive-exclude tests */*.xcworkspace/* 11 | recursive-include examples *.py 12 | recursive-include examples *.rb 13 | recursive-include examples *.txt 14 | 15 | global-exclude .DS_Store .idea *local-projects.txt 16 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: xcodeprojer 3 | Version: 0.1 4 | Summary: xcodeprojer is a Python script that brings your project.pbxproj files in order. 5 | Home-page: https://github.com/mikr/xcodeprojer 6 | Author: Michael Krause 7 | Author-email: michael@krause-software.com 8 | License: MIT 9 | Description: xcodeprojer 10 | ============= 11 | 12 | xcodeprojer is a Python script that brings your ``project.pbxproj`` files in order. 13 | It can transform any kind of JSON, XML or plist format into an internal 14 | representation and generate exactly the same commented plist format 15 | that Xcode itself uses. 16 | 17 | Without relying on Xcode at all this can be used for tasks like this: 18 | 19 | - Ensure the canonical plist format of the project before checking in a new version. 20 | - Perform custom modifications via these steps 21 | 22 | 1. transform your project into JSON or XML. 23 | 2. manipulate the JSON or XML object tree in a language of your choice. 24 | 3. emit JSON or XML and let xcodeprojer create the proper plist for you. 25 | - Perform custom modifications directly via Python with xcodeprojer as a module. 26 | - Repair broken projects, a failed parse reports the line and column where the parser failed. [#f1]_ 27 | - Doing all of the above even on a non-Mac computer because it is written in pure Python. 28 | 29 | xcodeprojer needs at least Python 2.7, the installation that comes with OS X works nicely 30 | and it works as well with Python 3.2, 3.3 and 3.4. It has no other requirements. 31 | 32 | Converting formats 33 | ------------------ 34 | 35 | To convert the ``project.pbxproj`` into the proper plist format of the same version (e.g. 46) 36 | simply run: 37 | 38 | .. code-block:: bash 39 | 40 | $ xcodeprojer --convert xcode project.pbxproj 41 | 42 | This should be equivalent to Xcode writing the file itself, e.g. by renaming a file within Xcode 43 | and renaming it back to actually trigger a project save. Let's call that the *canonical format*. 44 | 45 | You may also want to call xcodeprojer from another scripting language, for this 46 | we support reading the data from stdin and writing the output to stdout as follows: 47 | 48 | .. code-block:: bash 49 | 50 | $ cat project.pbxproj | xcodeprojer --convert xcode --projectname HelloWorld -o - 51 | 52 | In this case you should supply the project name which is the name of the parent directory of the ``project.pbxproj`` 53 | without the ``.xcodeproj`` suffix. We need this to create some of the comments. 54 | 55 | If the file you are trying to convert is in the filesystem, you can also run: 56 | 57 | .. code-block:: bash 58 | 59 | $ xcodeprojer --convert xcode -o - HelloWorld.xcodeproj/project.pbxproj 60 | 61 | and still get the result on stdout. 62 | 63 | Linting 64 | ---------- 65 | 66 | If you are already running some kind of buildbot or checkin hooks for quality control 67 | you can add something like 68 | 69 | .. code-block:: bash 70 | 71 | $ xcodeprojer --lint project.pbxproj 72 | 73 | which gives you feedback via the return code if the file is syntactically 74 | 75 | 0. perfectly in the canonical format. 76 | 1. parsable but not in the canonical format. 77 | 2. not parsable at all by Xcode. 78 | 79 | When linting several files at once, only the worst error code is returned. 80 | If you need more detail, please lint several files one after another. 81 | 82 | 83 | Global ids 84 | ---------- 85 | 86 | xcodeprojer has options to create the Xcode gids and take them apart. 87 | 88 | .. code-block:: bash 89 | 90 | $ xcodeprojer --giddump --gid-format=json project.pbxproj 91 | 92 | .. code-block:: json 93 | 94 | { 95 | "gids":[ 96 | { 97 | "comment":"Build configuration list for PBXProject \"MiniProject\"", 98 | "date":"2014-08-31T13:57:16Z", 99 | "gid":"4CDE969D19B3613C009DF310", 100 | "pid":222, 101 | "random":10351376, 102 | "seq":38557, 103 | "user":76 104 | } 105 | ] 106 | } 107 | 108 | Without the ``--gid-format`` a column layout of the same information is written that you might prefer when just want to look at the data. 109 | 110 | For a taste of what you can get by sorting and aggregating the Xcode ids 111 | run something like: 112 | 113 | .. code-block:: bash 114 | 115 | $ examples/gidhistograms.py /path/with/many/sampleprojects 116 | 117 | ``gidhistograms.py`` also has an ``--emoji`` option that shows the users 118 | working on the projects over the years in a compact format. 119 | 120 | Likewise you can conceal information about your own projects. 121 | You can generate ids to e.g. hide the number of users working 122 | on the project or in the timeframe in which they were doing so:: 123 | 124 | $ xcodeprojer --gid 2 --gid-pid 50000 --gid-user notme --gid-date 2007-01-09T16:41:00Z 125 | 0350F9550B53EF0C00A125FD 126 | 0350F9560B53EF0C00A125FD 127 | 128 | Syntax checking only 129 | ----------------------- 130 | 131 | The parser and unparser only care about the syntactic validity of the plist format. 132 | Xcode performs many checks and corrections based on the actual object types. 133 | This project is useful mostly for converting into the Xcode plist format and 134 | for correcting sloppy merges of the project including indentation, reordering 135 | and comment correction. 136 | 137 | Upgrading the file format version 138 | ------------------------------------ 139 | 140 | You should not use this script to change the file format version, e.g. from 43 to 46 141 | because ``isa`` types and attributes change between versions. 142 | The only reason to convert a file format into a different version is to 143 | prepare versions of the same project for a diff inspection to reduce syntactic noise 144 | much like you use 'ignore whitespace'. 145 | Please don't try to open a project file whose version you have changed with this script in Xcode. 146 | 147 | Installation 148 | --------------- 149 | 150 | Standalone script use 151 | ''''''''''''''''''''' 152 | 153 | If you are only planning to use this only as a shell script, don't bother with 154 | the Python way of installing things and just copy the script into a ``PATH`` 155 | of your choice while dropping the ``.py`` extension, e.g.: 156 | 157 | .. code-block:: bash 158 | 159 | $ sudo cp xcodeprojer.py /usr/local/bin/xcodeprojer 160 | 161 | Python module 162 | ''''''''''''' 163 | 164 | To use xcodeprojer.py as a module, ``cd`` into the xcodeprojer directory and install it with either 165 | 166 | .. code-block:: bash 167 | 168 | $ pip install -e . 169 | 170 | or 171 | 172 | .. code-block:: bash 173 | 174 | $ python setup.py install 175 | 176 | Here are some lines how to use xcodeprojer from Python 177 | 178 | .. code-block:: python 179 | 180 | import xcodeprojer 181 | 182 | filename = 'UICatalog.xcodeproj/project.pbxproj' 183 | prj = open(filename, 'rb').read() 184 | root, parseinfo = xcodeprojer.parse(prj) 185 | # The text nodes of a parse tree are always unicode strings 186 | # (unicode for Python 2, str for Python 3). 187 | xcodeprojer.report_parse_status(root, parseinfo, filename=filename) 188 | if root is not None: 189 | prjname = xcodeprojer.projectname_for_path(filename) 190 | # The result from xcodeprojer.unparse is always a UTF-8 encoded 191 | # byte string (str for Python 2, bytes for Python 3). 192 | output = xcodeprojer.unparse(root, format='xcode', projectname=prjname) 193 | 194 | The script ``examples/add_buildphase.py`` shows how to use xcodeprojer as a module 195 | to add a buildphase to one of the test projects. 196 | 197 | Ruby 198 | '''' 199 | 200 | The script ``examples/add_buildphase.rb`` is an example how you can call xcodeprojer as 201 | an external command while shuffling data back and forth in JSON with the end result 202 | in the canonical Xcode plist format. Most users who are already manipulating Xcode projects 203 | via scripts have all modifications up to the final JSON representation ready 204 | and may only want to use the last step of generating the commented plist format. 205 | 206 | 207 | Author 208 | ------ 209 | 210 | xcodeprojer was written by `Michael Krause `_. 211 | 212 | License 213 | ------- 214 | 215 | xcodeprojer is available under the `MIT license `_. See the LICENSE file for more info. 216 | 217 | .. rubric:: Footnotes 218 | 219 | .. [#f1] Of course Xcode has error reporting as well: 220 | 221 | .. code-block:: bash 222 | 223 | $ grep CFPropertyListCreateFromXMLData /var/log/system.log 224 | 225 | 226 | Keywords: xcode plist json xml 227 | Platform: any 228 | Classifier: Development Status :: 3 - Alpha 229 | Classifier: Environment :: Console 230 | Classifier: Environment :: MacOS X 231 | Classifier: Intended Audience :: Developers 232 | Classifier: License :: OSI Approved :: MIT License 233 | Classifier: Natural Language :: English 234 | Classifier: Operating System :: MacOS :: MacOS X 235 | Classifier: Operating System :: OS Independent 236 | Classifier: Programming Language :: Python :: 2.7 237 | Classifier: Programming Language :: Python :: 3.2 238 | Classifier: Programming Language :: Python :: 3.3 239 | Classifier: Programming Language :: Python :: 3.4 240 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 241 | Classifier: Topic :: Software Development :: Quality Assurance 242 | Classifier: Topic :: Utilities 243 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | xcodeprojer 2 | ============= 3 | 4 | xcodeprojer is a Python script that brings your ``project.pbxproj`` files in order. 5 | It can transform any kind of JSON, XML or plist format into an internal 6 | representation and generate exactly the same commented plist format 7 | that Xcode itself uses. 8 | 9 | Without relying on Xcode at all this can be used for tasks like this: 10 | 11 | - Ensure the canonical plist format of the project before checking in a new version. 12 | - Perform custom modifications via these steps 13 | 14 | 1. transform your project into JSON or XML. 15 | 2. manipulate the JSON or XML object tree in a language of your choice. 16 | 3. emit JSON or XML and let xcodeprojer create the proper plist for you. 17 | - Perform custom modifications directly via Python with xcodeprojer as a module. 18 | - Repair broken projects, a failed parse reports the line and column where the parser failed. [#f1]_ 19 | - Doing all of the above even on a non-Mac computer because it is written in pure Python. 20 | 21 | xcodeprojer needs at least Python 2.7, the installation that comes with OS X works nicely 22 | and it works as well with Python 3.2, 3.3 and 3.4. It has no other requirements. 23 | 24 | Converting formats 25 | ------------------ 26 | 27 | To convert the ``project.pbxproj`` into the proper plist format of the same version (e.g. 46) 28 | simply run: 29 | 30 | .. code-block:: bash 31 | 32 | $ xcodeprojer --convert xcode project.pbxproj 33 | 34 | This should be equivalent to Xcode writing the file itself, e.g. by renaming a file within Xcode 35 | and renaming it back to actually trigger a project save. Let's call that the *canonical format*. 36 | 37 | You may also want to call xcodeprojer from another scripting language, for this 38 | we support reading the data from stdin and writing the output to stdout as follows: 39 | 40 | .. code-block:: bash 41 | 42 | $ cat project.pbxproj | xcodeprojer --convert xcode --projectname HelloWorld -o - 43 | 44 | In this case you should supply the project name which is the name of the parent directory of the ``project.pbxproj`` 45 | without the ``.xcodeproj`` suffix. We need this to create some of the comments. 46 | 47 | If the file you are trying to convert is in the filesystem, you can also run: 48 | 49 | .. code-block:: bash 50 | 51 | $ xcodeprojer --convert xcode -o - HelloWorld.xcodeproj/project.pbxproj 52 | 53 | and still get the result on stdout. 54 | 55 | Linting 56 | ---------- 57 | 58 | If you are already running some kind of buildbot or checkin hooks for quality control 59 | you can add something like 60 | 61 | .. code-block:: bash 62 | 63 | $ xcodeprojer --lint project.pbxproj 64 | 65 | which gives you feedback via the return code if the file is syntactically 66 | 67 | 0. perfectly in the canonical format. 68 | 1. parsable but not in the canonical format. 69 | 2. not parsable at all by Xcode. 70 | 71 | When linting several files at once, only the worst error code is returned. 72 | If you need more detail, please lint several files one after another. 73 | 74 | 75 | Global ids 76 | ---------- 77 | 78 | xcodeprojer has options to create the Xcode gids and take them apart. 79 | 80 | .. code-block:: bash 81 | 82 | $ xcodeprojer --giddump --gid-format=json project.pbxproj 83 | 84 | .. code-block:: json 85 | 86 | { 87 | "gids":[ 88 | { 89 | "comment":"Build configuration list for PBXProject \"MiniProject\"", 90 | "date":"2014-08-31T13:57:16Z", 91 | "gid":"4CDE969D19B3613C009DF310", 92 | "pid":222, 93 | "random":10351376, 94 | "seq":38557, 95 | "user":76 96 | } 97 | ] 98 | } 99 | 100 | Without the ``--gid-format`` a column layout of the same information is written that you might prefer when just want to look at the data. 101 | 102 | For a taste of what you can get by sorting and aggregating the Xcode ids 103 | run something like: 104 | 105 | .. code-block:: bash 106 | 107 | $ examples/gidhistograms.py /path/with/many/sampleprojects 108 | 109 | ``gidhistograms.py`` also has an ``--emoji`` option that shows the users 110 | working on the projects over the years in a compact format. 111 | 112 | Likewise you can conceal information about your own projects. 113 | You can generate ids to e.g. hide the number of users working 114 | on the project or in the timeframe in which they were doing so:: 115 | 116 | $ xcodeprojer --gid 2 --gid-pid 50000 --gid-user notme --gid-date 2007-01-09T16:41:00Z 117 | 0350F9550B53EF0C00A125FD 118 | 0350F9560B53EF0C00A125FD 119 | 120 | Syntax checking only 121 | ----------------------- 122 | 123 | The parser and unparser only care about the syntactic validity of the plist format. 124 | Xcode performs many checks and corrections based on the actual object types. 125 | This project is useful mostly for converting into the Xcode plist format and 126 | for correcting sloppy merges of the project including indentation, reordering 127 | and comment correction. 128 | 129 | Upgrading the file format version 130 | ------------------------------------ 131 | 132 | You should not use this script to change the file format version, e.g. from 43 to 46 133 | because ``isa`` types and attributes change between versions. 134 | The only reason to convert a file format into a different version is to 135 | prepare versions of the same project for a diff inspection to reduce syntactic noise 136 | much like you use 'ignore whitespace'. 137 | Please don't try to open a project file whose version you have changed with this script in Xcode. 138 | 139 | Installation 140 | --------------- 141 | 142 | Standalone script use 143 | ''''''''''''''''''''' 144 | 145 | If you are only planning to use this only as a shell script, don't bother with 146 | the Python way of installing things and just copy the script into a ``PATH`` 147 | of your choice while dropping the ``.py`` extension, e.g.: 148 | 149 | .. code-block:: bash 150 | 151 | $ sudo cp xcodeprojer.py /usr/local/bin/xcodeprojer 152 | 153 | Python module 154 | ''''''''''''' 155 | 156 | To use xcodeprojer.py as a module, ``cd`` into the xcodeprojer directory and install it with either 157 | 158 | .. code-block:: bash 159 | 160 | $ pip install -e . 161 | 162 | or 163 | 164 | .. code-block:: bash 165 | 166 | $ python setup.py install 167 | 168 | Here are some lines how to use xcodeprojer from Python 169 | 170 | .. code-block:: python 171 | 172 | import xcodeprojer 173 | 174 | filename = 'UICatalog.xcodeproj/project.pbxproj' 175 | prj = open(filename, 'rb').read() 176 | root, parseinfo = xcodeprojer.parse(prj) 177 | # The text nodes of a parse tree are always unicode strings 178 | # (unicode for Python 2, str for Python 3). 179 | xcodeprojer.report_parse_status(root, parseinfo, filename=filename) 180 | if root is not None: 181 | prjname = xcodeprojer.projectname_for_path(filename) 182 | # The result from xcodeprojer.unparse is always a UTF-8 encoded 183 | # byte string (str for Python 2, bytes for Python 3). 184 | output = xcodeprojer.unparse(root, format='xcode', projectname=prjname) 185 | 186 | The script ``examples/add_buildphase.py`` shows how to use xcodeprojer as a module 187 | to add a buildphase to one of the test projects. 188 | 189 | Ruby 190 | '''' 191 | 192 | The script ``examples/add_buildphase.rb`` is an example how you can call xcodeprojer as 193 | an external command while shuffling data back and forth in JSON with the end result 194 | in the canonical Xcode plist format. Most users who are already manipulating Xcode projects 195 | via scripts have all modifications up to the final JSON representation ready 196 | and may only want to use the last step of generating the commented plist format. 197 | 198 | 199 | Author 200 | ------ 201 | 202 | xcodeprojer was written by `Michael Krause `_. 203 | 204 | License 205 | ------- 206 | 207 | xcodeprojer is available under the `MIT license `_. See the LICENSE file for more info. 208 | 209 | .. rubric:: Footnotes 210 | 211 | .. [#f1] Of course Xcode has error reporting as well: 212 | 213 | .. code-block:: bash 214 | 215 | $ grep CFPropertyListCreateFromXMLData /var/log/system.log 216 | 217 | -------------------------------------------------------------------------------- /examples/add_buildphase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | """An example how to add a buildphase with xcodeprojer.""" 10 | 11 | from __future__ import print_function 12 | 13 | import sys 14 | import codecs 15 | from os.path import abspath, dirname, join 16 | 17 | # Set up the Python path so we find the xcodeprojer module in the parent directory 18 | # relative to this file. 19 | sys.path.insert(1, dirname(dirname(abspath(__file__)))) 20 | 21 | import xcodeprojer 22 | 23 | PY2 = sys.version_info[0] == 2 24 | PY3 = sys.version_info[0] == 3 25 | 26 | 27 | INTL_PROJECT_FILENAME = '../tests/data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.pbxproj' 28 | OK = 0 29 | PARSING_FAILED = 1 30 | 31 | 32 | def here(): 33 | return dirname(abspath(__file__)) 34 | 35 | 36 | def rel(filename): 37 | return join(here(), filename) 38 | 39 | 40 | def getobj(root, gid): 41 | return root['objects'][gid] 42 | 43 | 44 | def find_isas(root, isa): 45 | for key, obj in root['objects'].items(): 46 | if obj['isa'] == isa: 47 | yield key, obj 48 | 49 | 50 | def find_first(root, isa): 51 | return find_isas(root, isa).next()[1] 52 | 53 | 54 | def main(): 55 | filename = rel(INTL_PROJECT_FILENAME) 56 | with open(filename, 'rb') as f: 57 | xcodeproj = f.read() 58 | 59 | root, parseinfo = xcodeprojer.parse(xcodeproj, format='xcode') 60 | xcodeprojer.report_parse_status(root, parseinfo, filename=filename) 61 | if root is None: 62 | return PARSING_FAILED 63 | 64 | gen = xcodeprojer.UniqueXcodeIDGenerator() 65 | 66 | pbxproject = find_first(root, 'PBXProject') 67 | firsttarget = getobj(root, pbxproject['targets'][0]) 68 | 69 | # Construct a new buildphase as any other JSON object 70 | newbuildphase = {'isa': 'PBXShellScriptBuildPhase', 71 | 'buildActionMask': '2147483647', 72 | 'files': [], 73 | 'inputPaths': [], 74 | 'outputPaths': [], 75 | 'runOnlyForDeploymentPostprocessing': '0', 76 | 'shellPath': '/bin/sh', 77 | 'shellScript': "echo 'A new buildphase says hi!'"} 78 | id_newbuildphase = gen.generate() 79 | root['objects'][id_newbuildphase] = newbuildphase 80 | firsttarget['buildPhases'].insert(0, id_newbuildphase) 81 | 82 | projectname = xcodeprojer.projectname_for_path(filename) 83 | proj = xcodeprojer.unparse(root, 84 | format='xcode', 85 | projectname=projectname, 86 | parseinfo=parseinfo) 87 | 88 | with open(filename, 'wb') as f: 89 | f.write(proj) 90 | 91 | xcodeprojer.print_diff(xcodeproj, proj, filename=filename) 92 | return OK 93 | 94 | if __name__ == '__main__': 95 | if PY3: 96 | sys.stdout = codecs.getwriter('utf8')(sys.stdout.buffer) 97 | sys.stderr = codecs.getwriter('utf8')(sys.stderr.buffer) 98 | sys.exit(main()) 99 | -------------------------------------------------------------------------------- /examples/add_buildphase.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | # 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | # 9 | # Here is a simple example how to read an Xcode project into a Ruby hash, 10 | # manipulate it and write it back in the canonical Xcode plist format. 11 | # In this example a new buildphase is added to a test project every time 12 | # this script is run. 13 | # 14 | 15 | require 'json' 16 | require 'open3' 17 | require 'tmpdir' 18 | require 'Shellwords' 19 | 20 | INTL_PROJECT_FILENAME = '../tests/data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.pbxproj' 21 | XCODEPROJER = File.join(File.dirname(File.dirname(File.absolute_path(__FILE__))), 'xcodeprojer.py') 22 | 23 | 24 | def rel(path) 25 | File.absolute_path(File.join(File.dirname(File.absolute_path(__FILE__)), path)) 26 | end 27 | 28 | def find_isas(root, isa) 29 | root['objects'].each do |key, obj| 30 | if obj['isa'] == isa 31 | yield key, obj 32 | end 33 | end 34 | end 35 | 36 | def find_first(root, isa) 37 | find_isas(root, isa) do |key, obj| 38 | return obj if obj['isa'] == isa 39 | end 40 | end 41 | 42 | def fetch_gids(n) 43 | cmd = %Q|#{XCODEPROJER.shellescape} --gid #{n.to_s.shellescape}| 44 | stdout_str, stderr_str, status = Open3.capture3(cmd) 45 | stdout_str 46 | end 47 | 48 | def convert(projectdata, format, projectname) 49 | cmd = %Q|#{XCODEPROJER.shellescape} --convert #{format.shellescape} --projectname #{projectname.shellescape} -o -| 50 | stdout_str, stderr_str, status = Open3.capture3(cmd, :stdin_data=>projectdata) 51 | if status.exitstatus == 0 52 | stdout_str 53 | else 54 | STDERR.puts stderr_str 55 | nil 56 | end 57 | end 58 | 59 | def rundiff(old, new) 60 | cmd = %Q|/usr/bin/diff -u #{old.shellescape} #{new.shellescape}| 61 | puts cmd 62 | stdout_str, stderr_str, status = Open3.capture3(cmd) 63 | puts stdout_str 64 | end 65 | 66 | def getobj(root, gid) 67 | root['objects'][gid] 68 | end 69 | 70 | 71 | pbxfilename = rel(INTL_PROJECT_FILENAME) 72 | qpbxfilename = pbxfilename.shellescape 73 | 74 | xcodeproj = File.basename(File.dirname(pbxfilename)) 75 | ext = File.extname(xcodeproj) 76 | 77 | if ['.xcodeproj', '.xcode', '.pbproj', '.pbxproj'].include? ext 78 | projname = File.basename(xcodeproj, ext) 79 | else 80 | STDERR.puts %Q|Cannot determine the project name from the parent directory of #{qpbxfilename}| 81 | exit(1) 82 | end 83 | 84 | inputfile = File.open(pbxfilename, 'rb') 85 | contents = inputfile.read 86 | inputfile.close 87 | jsondata = convert(contents, 'json', projname) 88 | 89 | if jsondata.nil? 90 | STDERR.puts "Converting #{pbxfilename} to JSON failed." 91 | exit(1) 92 | end 93 | 94 | root = JSON.load(jsondata) 95 | 96 | pbxproject = find_first(root, 'PBXProject') 97 | 98 | if pbxproject 99 | # Fetch as many gids as you like 100 | freshgids = fetch_gids(5).lines 101 | firsttarget = getobj(root, pbxproject['targets'][0]) 102 | 103 | # Construct a new buildphase as any other JSON object 104 | newbuildphase = {"isa" => "PBXShellScriptBuildPhase", 105 | "buildActionMask" => "2147483647", 106 | "files" => [], 107 | "inputPaths" => [], 108 | "outputPaths" => [], 109 | "runOnlyForDeploymentPostprocessing" => "0", 110 | "shellPath" => "/bin/sh", 111 | "shellScript" => 'echo "A new buildphase says hi!"'} 112 | id_newbuildphase = freshgids[0] 113 | root['objects'][id_newbuildphase] = newbuildphase 114 | firsttarget['buildPhases'].insert(0, id_newbuildphase) 115 | newjsondata = JSON.generate(root) 116 | xcodeprojectdata = convert(newjsondata, 'xcode', projname) 117 | 118 | puts %Q|Adding a new buildphase with a dynamically generated Xcode id to #{qpbxfilename}| 119 | puts 120 | 121 | tmpname = File.join(Dir.tmpdir, Dir::Tmpname.make_tmpname(['projer', '.pbxproj'], nil)) 122 | projbackup = File.open(tmpname, 'wb') 123 | begin 124 | projbackup.write(contents) 125 | puts %Q|Saved the old contents of #{qpbxfilename} to #{tmpname.shellescape}| 126 | ensure 127 | projbackup.close 128 | end 129 | 130 | outputfile = File.open(pbxfilename, 'wb') 131 | outputfile.write(xcodeprojectdata) 132 | outputfile.close 133 | 134 | rundiff(tmpname, pbxfilename) 135 | else 136 | STDERR.puts "Did not find a PBXProject." 137 | exit(1) 138 | end 139 | -------------------------------------------------------------------------------- /examples/emojis.txt: -------------------------------------------------------------------------------- 1 | # Here are some emoji characters that should be visible 2 | # against a black background. 3 | # All characters with code points < 0x100 are filtered out. 4 | # You can add, remove and rearrange them as you see fit. 5 | # Only the first 256 emoji characters are used in the program 6 | # the rest are spares for customization. 7 | 8 | 🌀 🌁 🌂 🌃 🌄 🌇 🌈 🌉 🌊 🌋 🌌 🌜 🌝 🌞 🌟 🌠 🌰 🌱 🌲 🌳 🌴 🌵 🌷 🌸 🌹 🌺 🌻 🌼 🌽 🌾 🌿 🍀 9 | 10 | 🍁 🍂 🍃 🍄 🍅 🍆 🍇 🍈 🍉 🍊 🍋 🍌 🍍 🍎 🍏 🍐 🍑 🍒 🍓 🍔 🍕 🍖 🍗 🍘 🍙 🍚 🍛 🍜 🍝 🍞 🍟 🍠 11 | 12 | 🍡 🍢 🍥 🍦 🍧 🍨 🍩 🍪 🍫 🍬 🍭 🍮 🍯 🍰 🍱 🍲 🍳 🍴 🍵 🍶 🍷 🍸 🍹 🍺 🎀 🎁 🎂 🎃 🎄 🎅 🎆 🎉 13 | 14 | 🎍 🎏 🎐 🎒 🎓 🎠 🎡 🎢 🎤 🎧 🎨 🎩 🎪 🎫 🎬 🎭 🎮 🎯 🎱 🎲 🎳 🎵 🎷 🎸 🎹 🎺 🎻 🎾 🎿 🏀 🏁 🏂 15 | 16 | 🏄 🏆 🏇 🏈 🏉 🏊 🏡 🏤 🏫 🏮 🐅 🐇 🐈 🐊 🐋 🐌 🐍 🐎 🐓 🐙 🐚 🐛 🐝 🐞 🐟 🐠 🐡 🐢 🐧 🐨 🐩 🐬 17 | 18 | 🐭 🐯 🐰 🐱 🐲 🐳 🐸 🐹 🐼 🐾 👀 👆 👏 👑 👒 👓 👔 👕 👖 👟 👣 👧 👨 👩 👱 👲 👳 👻 👾 💄 💈 💉 19 | 20 | 💊 💌 💍 💎 💐 💠 💡 💢 💥 💧 💫 💬 💭 💺 💻 💼 💽 💾 📅 📇 📈 📉 📊 📋 📌 📍 📎 📏 📐 📑 📒 📔 21 | 22 | 📕 📖 📗 📘 📙 📚 📛 📜 📝 📠 📡 📢 📬 📮 📯 📰 📷 📹 📺 📻 🔆 🔋 🔏 🔐 🔑 🔔 🔖 🔗 🔥 🔦 🔧 🔨 23 | 24 | 🔩 🔬 🔭 🔮 🔰 🔱 🔴 🔵 🔶 🔷 🕓 🗻 🗼 🗽 🗾 😼 🚀 🚁 🚂 🚃 🚌 🚍 🚎 🚏 🚐 🚑 🚒 🚓 🚔 🚕 🚖 🚗 25 | 26 | 🚘 🚚 🚛 🚜 🚝 🚞 🚟 🚠 🚣 🚤 🚥 🚦 🚧 🚩 🚪 🚴 27 | -------------------------------------------------------------------------------- /examples/examine_local_projects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | """Test local project.pbxproj files. 10 | 11 | This script basically runs a lint over many Xcode project files and 12 | reports every file when the unparse of the parse look different 13 | then the original project file contents. 14 | 15 | To use it first run 16 | $ test_local_projects.py --find 17 | 18 | which creates local-projects.txt in the tests directory containing 19 | filenames that look like valid project files. 20 | 21 | Then run 22 | $ test_local_projects.py --test 23 | 24 | which reports the findings about the filenames listed in local-projects.txt. 25 | """ 26 | 27 | from __future__ import print_function 28 | 29 | import sys 30 | import argparse 31 | import time 32 | import codecs 33 | import types 34 | import os 35 | from os.path import abspath, dirname 36 | 37 | from io import StringIO 38 | 39 | import multiprocessing 40 | import traceback 41 | import errno 42 | from collections import namedtuple 43 | 44 | import utils 45 | 46 | # Set up the Python path so we find the xcodeprojer module in the parent directory 47 | # relative to this file. 48 | sys.path.insert(1, dirname(dirname(abspath(__file__)))) 49 | 50 | import xcodeprojer 51 | from xcodeprojer import bytestr, unistr 52 | 53 | PY2 = sys.version_info[0] == 2 54 | PY3 = sys.version_info[0] == 3 55 | 56 | 57 | LISTFILENAME = 'local-projects.txt' 58 | IGNOREDFILENAME = 'ignored-local-projects.txt' 59 | 60 | PBXPROJNAME = 'project.pbxproj' 61 | 62 | ExcInfo = namedtuple('ExcInfo', 'exc_info') 63 | 64 | LintResult = namedtuple('LintResult', ['filename', 'success', 'text', 'parsetime', 'unparsetime', 'numbytes']) 65 | 66 | if PY2: 67 | exec ('def reraise(tp, value, tb):\n raise tp, value, tb') 68 | else: 69 | def reraise(tp, value, tb): 70 | raise value.with_traceback(tb) 71 | 72 | 73 | def rel(filename): 74 | return os.path.join(dirname(abspath(__file__)), filename) 75 | 76 | 77 | def write(s='', end='\n'): 78 | s = unistr(s) + unistr(end) 79 | s = s.encode('utf-8') 80 | sys.stdout.write(s) 81 | 82 | 83 | def handle_file(pbxfilename, parsertype='normal'): 84 | try: 85 | with open(bytestr(pbxfilename), 'rb') as f: 86 | xcodeproj = f.read() 87 | t0 = time.time() 88 | root, parseinfo = xcodeprojer.parse(xcodeproj, dictionarytype=dict, parsertype=parsertype) 89 | buf = StringIO() 90 | xcodeprojer.report_parse_status(root, parseinfo, filename=pbxfilename, fp=buf) 91 | if root is None: 92 | return LintResult(pbxfilename, False, buf.getvalue(), 0, 0, len(xcodeproj)) 93 | t1 = time.time() 94 | projname = xcodeprojer.projectname_for_path(pbxfilename) 95 | text = xcodeprojer.unparse(root, format='xcode', projectname=projname, parseinfo=parseinfo) 96 | t2 = time.time() 97 | return LintResult(pbxfilename, True, text, t1-t0, t2-t1, len(xcodeproj)) 98 | except Exception as e: 99 | e.traceback = traceback.format_exc() 100 | raise 101 | 102 | 103 | def filenames_to_examine(): 104 | xcodeprojects = projects_from_list() 105 | ignored_projects = set() 106 | try: 107 | ignxcodeprojects = codecs.open(rel(IGNOREDFILENAME), 'r', encoding='utf-8').read() 108 | for filename in ignxcodeprojects.strip().splitlines(): 109 | ignored_projects.add(filename) 110 | except IOError: 111 | pass 112 | 113 | projects_filenames = xcodeprojects 114 | filenames = [x for x in projects_filenames if x not in ignored_projects] 115 | return filenames 116 | 117 | 118 | def run_lint(args, filtered_idx_filenames): 119 | use_pool = not args.disable_parallel 120 | 121 | if not use_pool: 122 | for pbxfilename in filtered_idx_filenames: 123 | try: 124 | yield handle_file(pbxfilename, parsertype=args.parser) 125 | except Exception: 126 | yield ExcInfo(sys.exc_info()) 127 | 128 | if use_pool: 129 | pool = multiprocessing.Pool(initializer=utils.per_process_init) 130 | try: 131 | async_results = [pool.apply_async(handle_file, [x], {'parsertype': args.parser}) for x in filtered_idx_filenames] 132 | pool.close() 133 | while async_results: 134 | try: 135 | asyncres = async_results.pop(0) 136 | yield asyncres.get() 137 | except (KeyboardInterrupt, GeneratorExit): 138 | raise 139 | except Exception as e: 140 | t, v, tb = sys.exc_info() 141 | try: 142 | # Report the textual traceback of the subprocess rather than 143 | # this local exception that was triggered by the other side. 144 | tb = e.traceback 145 | except AttributeError: 146 | pass 147 | yield ExcInfo((t, v, tb)) 148 | except (KeyboardInterrupt, GeneratorExit): 149 | pool.terminate() 150 | finally: 151 | pool.join() 152 | 153 | 154 | def examine_projects(args): 155 | start_index = args.start_index 156 | max_files = args.max_files 157 | 158 | filenames = filenames_to_examine() 159 | filenames = filenames[start_index:] 160 | if max_files is not None: 161 | filenames = filenames[:max_files] 162 | 163 | total_numbytes = 0 164 | total_parsetime = 0 165 | total_unparsetime = 0 166 | num_files = 0 167 | num_successes = 0 168 | t0 = time.time() 169 | for idx, result in enumerate(run_lint(args, filenames)): 170 | num_files += 1 171 | globalidx = start_index + idx 172 | try: 173 | tbtext = None 174 | if isinstance(result, ExcInfo): 175 | t, v, tb = result.exc_info 176 | if not isinstance(tb, types.TracebackType): 177 | tbtext = tb 178 | tb = None 179 | reraise(t, v, tb) 180 | 181 | sys.stdout.write("%d " % globalidx) 182 | sys.stdout.flush() 183 | handle_result(args, result.success, result.text, result.filename) 184 | if result.success: 185 | num_successes += 1 186 | if args.reportstats: 187 | total_numbytes += result.numbytes 188 | total_parsetime += result.parsetime 189 | total_unparsetime += result.unparsetime 190 | except IOError as e: 191 | write('\n%d "%s" failed: %s' % (globalidx, unistr(filenames[idx]), repr(e))) 192 | except Exception as e: 193 | write('\n%d "%s" failed:' % (globalidx, unistr(filenames[idx]))) 194 | if tbtext is not None: 195 | print(tbtext) 196 | else: 197 | traceback.print_exc() 198 | 199 | if args.reportstats and num_successes > 0: 200 | tdelta = time.time() - t0 201 | print("\nparse rate:%9d Bps unparse rate:%9d Bps (per core)" % (total_numbytes / total_parsetime, total_numbytes / total_unparsetime)) 202 | print("Processed %d Bps, avg. time per project: %f" % (total_numbytes / tdelta, tdelta / num_successes)) 203 | if args.reportstats: 204 | print("Processed %d project files of which %d were unsuccessful" % (num_files, num_files - num_successes)) 205 | 206 | 207 | def handle_result(args, success, text, filename): 208 | if not success: 209 | print() 210 | print(text) 211 | return 212 | 213 | try: 214 | with open(bytestr(filename), 'rb') as f: 215 | origtext = f.read() 216 | if origtext[:1] not in [b'/', b'{']: 217 | # Only handle files in plist format. 218 | return 219 | except IOError as e: 220 | if e.errno not in (errno.ENOTDIR, errno.ENOENT): 221 | raise 222 | return 223 | 224 | if text == origtext: 225 | return 226 | 227 | xcodeprojer.print_diff(origtext, text, difftype=args.diff, filename=filename) 228 | 229 | 230 | def find_projects(args, parser): 231 | root = args.find 232 | filenames = [] 233 | for name in xcodeprojer.find_projectfiles(root): 234 | filenames.append(name) 235 | # This might take a while, report progress 236 | sys.stdout.write('.') 237 | sys.stdout.flush() 238 | print() 239 | 240 | if not filenames: 241 | print('No project.pbxproj files found in "%s"' % root) 242 | return 243 | 244 | fn = rel(LISTFILENAME) 245 | with open(fn, 'wb') as f: 246 | text = '\n'.join(filenames) + '\n' 247 | f.write(bytestr(text)) 248 | print('\nWrote %d filename to "%s"' % (len(filenames), fn)) 249 | 250 | def projects_from_list(): 251 | filename = rel(LISTFILENAME) 252 | with codecs.open(filename, 'r', encoding='utf-8') as f: 253 | return f.read().splitlines() 254 | 255 | def examine_filelist(args, parser): 256 | filename = rel(LISTFILENAME) 257 | filelist = [] 258 | try: 259 | filelist = projects_from_list() 260 | errmsg = 'does not contain any filenames.' 261 | except IOError: 262 | errmsg = 'does not exist or is not readable.' 263 | 264 | if len(filelist) < 1: 265 | print('"%s" %s\n' 266 | 'If you could run something like:\n' 267 | ' %s --find /some/path/with/project/files/beneath\n' 268 | 'before running the test, so we know about some project files to examine,' 269 | ' that would be great.' % (filename, errmsg, sys.argv[0])) 270 | return 271 | 272 | t0 = time.time() 273 | examine_projects(args) 274 | t1 = time.time() - t0 275 | print() 276 | if args.reportstats: 277 | print("Elapsed time: %f seconds" % t1) 278 | 279 | 280 | def main(): 281 | parser = argparse.ArgumentParser(description='Find and test local project files.') 282 | parser.add_argument('--parser', choices=['normal', 'fast', 'classic'], default='normal') 283 | parser.add_argument('-f', '--find', metavar='PATH', help='find local project files') 284 | parser.add_argument('-t', '--test', action='store_true', help='run all tests') 285 | parser.add_argument('-s', '--start-index', action='store', type=int, dest='start_index', default=0) 286 | parser.add_argument('-n', '--max-files', action='store', type=int, dest='max_files', help='maximum number of files to process') 287 | parser.add_argument('-d', '--disable-parallel', action='store_true', help='do not run tests in parallel') 288 | parser.add_argument('--diff', choices=['unified', 'html', 'opendiff'], default='opendiff', 289 | help='how to display the diffs') 290 | parser.add_argument('--reportstats', action='store_true', help='print performance statistics') 291 | parser.add_argument('--profile', action='store_true', help='run everything through the profiler') 292 | 293 | args = parser.parse_args() 294 | 295 | num_actions = 0 296 | actions = 'find test'.split() 297 | for act in actions: 298 | if getattr(args, act): 299 | num_actions += 1 300 | 301 | if num_actions != 1: 302 | parser.error('Please specify exactly one of the options %s.' % ', '.join('--' + x for x in actions)) 303 | 304 | if args.profile: 305 | print('Profiling...') 306 | utils.profile('call_command(args, parser)', locals(), globals()) 307 | else: 308 | call_command(args, parser) 309 | 310 | 311 | def call_command(args, parser): 312 | if args.find: 313 | find_projects(args, parser) 314 | elif args.test: 315 | examine_filelist(args, parser) 316 | else: 317 | parser.error('Something is wrong with the options or the handling of them.') 318 | 319 | 320 | if __name__ == '__main__': 321 | if PY3: 322 | sys.stdout = codecs.getwriter('utf8')(sys.stdout.buffer) 323 | sys.stderr = codecs.getwriter('utf8')(sys.stderr.buffer) 324 | main() 325 | -------------------------------------------------------------------------------- /examples/firstnames.txt: -------------------------------------------------------------------------------- 1 | james 2 | john 3 | robert 4 | michael 5 | mary 6 | william 7 | david 8 | richard 9 | charles 10 | joseph 11 | thomas 12 | patricia 13 | christopher 14 | linda 15 | barbara 16 | daniel 17 | paul 18 | mark 19 | elizabeth 20 | donald 21 | jennifer 22 | george 23 | maria 24 | kenneth 25 | susan 26 | steven 27 | edward 28 | margaret 29 | brian 30 | ronald 31 | dorothy 32 | anthony 33 | lisa 34 | kevin 35 | nancy 36 | karen 37 | betty 38 | helen 39 | jason 40 | matthew 41 | gary 42 | timothy 43 | sandra 44 | jose 45 | larry 46 | jeffrey 47 | frank 48 | donna 49 | carol 50 | ruth 51 | scott 52 | eric 53 | stephen 54 | andrew 55 | sharon 56 | michelle 57 | laura 58 | sarah 59 | kimberly 60 | deborah 61 | jessica 62 | raymond 63 | shirley 64 | cynthia 65 | angela 66 | melissa 67 | brenda 68 | amy 69 | jerry 70 | gregory 71 | anna 72 | joshua 73 | virginia 74 | rebecca 75 | kathleen 76 | dennis 77 | pamela 78 | martha 79 | debra 80 | amanda 81 | walter 82 | stephanie 83 | willie 84 | patrick 85 | terry 86 | carolyn 87 | peter 88 | christine 89 | marie 90 | janet 91 | frances 92 | catherine 93 | harold 94 | henry 95 | douglas 96 | joyce 97 | ann 98 | diane 99 | alice 100 | jean 101 | julie 102 | carl 103 | kelly 104 | heather 105 | arthur 106 | teresa 107 | gloria 108 | doris 109 | ryan 110 | joe 111 | roger 112 | evelyn 113 | juan 114 | ashley 115 | jack 116 | cheryl 117 | albert 118 | joan 119 | mildred 120 | katherine 121 | justin 122 | jonathan 123 | gerald 124 | keith 125 | samuel 126 | judith 127 | rose 128 | janice 129 | lawrence 130 | ralph 131 | nicole 132 | judy 133 | nicholas 134 | christina 135 | roy 136 | kathy 137 | theresa 138 | benjamin 139 | beverly 140 | denise 141 | bruce 142 | brandon 143 | adam 144 | tammy 145 | irene 146 | fred 147 | billy 148 | harry 149 | jane 150 | wayne 151 | louis 152 | lori 153 | steve 154 | tracy 155 | jeremy 156 | rachel 157 | andrea 158 | aaron 159 | marilyn 160 | robin 161 | randy 162 | leslie 163 | kathryn 164 | eugene 165 | bobby 166 | howard 167 | carlos 168 | sara 169 | louise 170 | jacqueline 171 | anne 172 | wanda 173 | russell 174 | shawn 175 | victor 176 | julia 177 | bonnie 178 | ruby 179 | chris 180 | tina 181 | lois 182 | phyllis 183 | jamie 184 | norma 185 | martin 186 | paula 187 | jesse 188 | diana 189 | annie 190 | shannon 191 | ernest 192 | todd 193 | phillip 194 | lee 195 | lillian 196 | peggy 197 | emily 198 | crystal 199 | kim 200 | craig 201 | carmen 202 | gladys 203 | connie 204 | rita 205 | alan 206 | dawn 207 | florence 208 | dale 209 | sean 210 | francis 211 | johnny 212 | clarence 213 | philip 214 | edna 215 | tiffany 216 | tony 217 | rosa 218 | jimmy 219 | earl 220 | cindy 221 | antonio 222 | luis 223 | mike 224 | danny 225 | bryan 226 | grace 227 | stanley 228 | leonard 229 | wendy 230 | nathan 231 | manuel 232 | curtis 233 | victoria 234 | rodney 235 | norman 236 | edith 237 | sherry 238 | sylvia 239 | josephine 240 | allen 241 | thelma 242 | sheila 243 | ethel 244 | marjorie 245 | lynn 246 | ellen 247 | elaine 248 | marvin 249 | carrie 250 | marion 251 | charlotte 252 | vincent 253 | glenn 254 | travis 255 | monica 256 | jeffery 257 | jeff 258 | esther 259 | pauline 260 | jacob 261 | emma 262 | chad 263 | kyle 264 | juanita 265 | dana 266 | melvin 267 | jessie 268 | rhonda 269 | anita 270 | alfred 271 | hazel 272 | amber 273 | eva 274 | bradley 275 | ray 276 | jesus 277 | debbie 278 | herbert 279 | eddie 280 | joel 281 | frederick 282 | april 283 | lucille 284 | clara 285 | gail 286 | joanne 287 | eleanor 288 | valerie 289 | danielle 290 | erin 291 | edwin 292 | megan 293 | alicia 294 | suzanne 295 | michele 296 | don 297 | bertha 298 | veronica 299 | jill 300 | darlene 301 | ricky 302 | lauren 303 | geraldine 304 | troy 305 | stacy 306 | randall 307 | cathy 308 | joann 309 | sally 310 | lorraine 311 | barry 312 | alexander 313 | regina 314 | jackie 315 | erica 316 | beatrice 317 | dolores 318 | bernice 319 | mario 320 | bernard 321 | audrey 322 | yvonne 323 | francisco 324 | micheal 325 | leroy 326 | june 327 | annette 328 | samantha 329 | marcus 330 | theodore 331 | oscar 332 | clifford 333 | miguel 334 | jay 335 | renee 336 | ana 337 | vivian 338 | jim 339 | ida 340 | tom 341 | ronnie 342 | roberta 343 | holly 344 | brittany 345 | angel 346 | alex 347 | melanie 348 | jon 349 | yolanda 350 | tommy 351 | loretta 352 | jeanette 353 | calvin 354 | laurie 355 | leon 356 | katie 357 | stacey 358 | lloyd 359 | derek 360 | bill 361 | vanessa 362 | sue 363 | kristen 364 | alma 365 | warren 366 | elsie 367 | beth 368 | vicki 369 | jeanne 370 | jerome 371 | darrell 372 | tara 373 | rosemary 374 | leo 375 | floyd 376 | dean 377 | carla 378 | wesley 379 | terri 380 | eileen 381 | courtney 382 | alvin 383 | tim 384 | jorge 385 | greg 386 | gordon 387 | pedro 388 | lucy 389 | gertrude 390 | dustin 391 | derrick 392 | corey 393 | tonya 394 | dan 395 | ella 396 | lewis 397 | zachary 398 | wilma 399 | maurice 400 | kristin 401 | gina 402 | vernon 403 | vera 404 | roberto 405 | natalie 406 | clyde 407 | agnes 408 | herman 409 | charlene 410 | charlie 411 | bessie 412 | shane 413 | delores 414 | sam 415 | pearl 416 | melinda 417 | hector 418 | glen 419 | arlene 420 | ricardo 421 | tamara 422 | maureen 423 | lester 424 | gene 425 | colleen 426 | allison 427 | tyler 428 | rick 429 | joy 430 | johnnie 431 | georgia 432 | constance 433 | ramon 434 | marcia 435 | lillie 436 | claudia 437 | brent 438 | tanya 439 | nellie 440 | minnie 441 | gilbert 442 | marlene 443 | heidi 444 | glenda 445 | marc 446 | viola 447 | marian 448 | lydia 449 | billie 450 | stella 451 | guadalupe 452 | caroline 453 | reginald 454 | dora 455 | jo 456 | cecil 457 | casey 458 | brett 459 | vickie 460 | ruben 461 | jaime 462 | rafael 463 | nathaniel 464 | mattie 465 | milton 466 | edgar 467 | raul 468 | maxine 469 | irma 470 | myrtle 471 | marsha 472 | mabel 473 | chester 474 | ben 475 | andre 476 | adrian 477 | lena 478 | franklin 479 | duane 480 | christy 481 | tracey 482 | patsy 483 | gabriel 484 | deanna 485 | jimmie 486 | hilda 487 | elmer 488 | christian 489 | bobbie 490 | gwendolyn 491 | nora 492 | mitchell 493 | jennie 494 | brad 495 | ron 496 | roland 497 | nina 498 | margie 499 | leah 500 | harvey 501 | cory 502 | cassandra 503 | arnold 504 | priscilla 505 | penny 506 | naomi 507 | kay 508 | karl 509 | jared 510 | carole 511 | olga 512 | jan 513 | brandy 514 | lonnie 515 | leona 516 | dianne 517 | claude 518 | sonia 519 | jordan 520 | jenny 521 | felicia 522 | erik 523 | lindsey 524 | kerry 525 | darryl 526 | velma 527 | neil 528 | miriam 529 | becky 530 | violet 531 | kristina 532 | javier 533 | fernando 534 | cody 535 | clinton 536 | tyrone 537 | toni 538 | ted 539 | rene 540 | mathew 541 | lindsay 542 | julio 543 | darren 544 | misty 545 | mae 546 | lance 547 | sherri 548 | shelly 549 | sandy 550 | ramona 551 | pat 552 | kurt 553 | jody 554 | daisy 555 | nelson 556 | katrina 557 | erika 558 | claire 559 | allan 560 | hugh 561 | guy 562 | clayton 563 | sheryl 564 | max 565 | margarita 566 | geneva 567 | dwayne 568 | belinda 569 | felix 570 | faye 571 | dwight 572 | cora 573 | armando 574 | sabrina 575 | natasha 576 | isabel 577 | everett 578 | ada 579 | wallace 580 | sidney 581 | marguerite 582 | ian 583 | hattie 584 | harriet 585 | rosie 586 | molly 587 | kristi 588 | ken 589 | joanna 590 | iris 591 | cecilia 592 | brandi 593 | bob 594 | blanche 595 | julian 596 | eunice 597 | angie 598 | alfredo 599 | lynda 600 | ivan 601 | inez 602 | freddie 603 | dave 604 | alberto 605 | madeline 606 | daryl 607 | byron 608 | amelia 609 | alberta 610 | sonya 611 | perry 612 | morris 613 | monique 614 | maggie 615 | kristine 616 | kayla 617 | jodi 618 | janie 619 | isaac 620 | genevieve 621 | candace 622 | yvette 623 | willard 624 | whitney 625 | virgil 626 | ross 627 | opal 628 | melody 629 | maryann 630 | marshall 631 | fannie 632 | clifton 633 | alison 634 | susie 635 | shelley 636 | sergio 637 | salvador 638 | olivia 639 | luz 640 | kirk 641 | flora 642 | andy 643 | verna 644 | terrance 645 | seth 646 | mamie 647 | lula 648 | lola 649 | kristy 650 | kent 651 | beulah 652 | antoinette 653 | terrence 654 | gayle 655 | eduardo 656 | pam 657 | kelli 658 | juana 659 | joey 660 | jeannette 661 | enrique 662 | donnie 663 | candice 664 | wade 665 | hannah 666 | frankie 667 | bridget 668 | austin 669 | stuart 670 | karla 671 | evan 672 | celia 673 | vicky 674 | shelia 675 | patty 676 | nick 677 | lynne 678 | luther 679 | latoya 680 | fredrick 681 | della 682 | arturo 683 | alejandro 684 | wendell 685 | sheri 686 | marianne 687 | julius 688 | jeremiah 689 | shaun 690 | otis 691 | kara 692 | jacquelyn 693 | erma 694 | blanca 695 | angelo 696 | alexis 697 | trevor 698 | roxanne 699 | oliver 700 | myra 701 | morgan 702 | luke 703 | leticia 704 | krista 705 | homer 706 | gerard 707 | doug 708 | cameron 709 | sadie 710 | rosalie 711 | robyn 712 | kenny 713 | ira 714 | hubert 715 | brooke 716 | bethany 717 | bernadette 718 | bennie 719 | antonia 720 | angelica 721 | alexandra 722 | adrienne 723 | traci 724 | rachael 725 | nichole 726 | muriel 727 | matt 728 | mable 729 | lyle 730 | laverne 731 | kendra 732 | jasmine 733 | ernestine 734 | chelsea 735 | alfonso 736 | rex 737 | orlando 738 | ollie 739 | neal 740 | marcella 741 | loren 742 | krystal 743 | ernesto 744 | elena 745 | carlton 746 | blake 747 | angelina 748 | wilbur 749 | taylor 750 | shelby 751 | rudy 752 | roderick 753 | paulette 754 | pablo 755 | omar 756 | noel 757 | nadine 758 | lorenzo 759 | lora 760 | leigh 761 | kari 762 | horace 763 | grant 764 | estelle 765 | dianna 766 | willis 767 | rosemarie 768 | rickey 769 | mona 770 | kelley 771 | doreen 772 | desiree 773 | abraham 774 | rudolph 775 | preston 776 | malcolm 777 | kelvin 778 | johnathan 779 | janis 780 | hope 781 | ginger 782 | freda 783 | damon 784 | christie 785 | cesar 786 | betsy 787 | andres 788 | wm 789 | tommie 790 | teri 791 | robbie 792 | meredith 793 | mercedes 794 | marco 795 | lynette 796 | eula 797 | cristina 798 | archie 799 | alton 800 | sophia 801 | rochelle 802 | randolph 803 | pete 804 | merle 805 | meghan 806 | jonathon 807 | gretchen 808 | gerardo 809 | geoffrey 810 | garry 811 | felipe 812 | eloise 813 | ed 814 | dominic 815 | devin 816 | cecelia 817 | carroll 818 | raquel 819 | lucas 820 | jana 821 | henrietta 822 | gwen 823 | guillermo 824 | earnest 825 | delbert 826 | colin 827 | alyssa 828 | tricia 829 | tasha 830 | spencer 831 | rodolfo 832 | olive 833 | myron 834 | jenna 835 | edmund 836 | cleo 837 | benny 838 | sophie 839 | sonja 840 | silvia 841 | salvatore 842 | patti 843 | mindy 844 | may 845 | mandy 846 | lowell 847 | lorena 848 | lila 849 | lana 850 | kellie 851 | kate 852 | jewel 853 | gregg 854 | garrett 855 | essie 856 | elvira 857 | delia 858 | darla 859 | cedric 860 | wilson 861 | sylvester 862 | sherman 863 | shari 864 | roosevelt 865 | miranda 866 | marty 867 | marta 868 | lucia 869 | lorene 870 | lela 871 | josefina 872 | johanna 873 | jermaine 874 | jeannie 875 | israel 876 | faith 877 | elsa 878 | dixie 879 | camille 880 | winifred 881 | wilbert 882 | tami 883 | tabitha 884 | shawna 885 | rena 886 | ora 887 | nettie 888 | melba 889 | marina 890 | leland 891 | kristie 892 | forrest 893 | elisa 894 | ebony 895 | alisha 896 | aimee 897 | tammie 898 | simon 899 | sherrie 900 | sammy 901 | ronda 902 | patrice 903 | owen 904 | myrna 905 | marla 906 | latasha 907 | irving 908 | dallas 909 | clark 910 | bryant 911 | bonita 912 | aubrey 913 | addie 914 | woodrow 915 | stacie 916 | rufus 917 | rosario 918 | rebekah 919 | marcos 920 | mack 921 | lupe 922 | lucinda 923 | lou 924 | levi 925 | laurence 926 | kristopher 927 | jewell 928 | jake 929 | gustavo 930 | francine 931 | ellis 932 | drew 933 | dorthy 934 | deloris 935 | cheri 936 | celeste 937 | cara 938 | adriana 939 | adele 940 | abigail 941 | trisha 942 | trina 943 | tracie 944 | sallie 945 | reba 946 | orville 947 | nikki 948 | nicolas 949 | marissa 950 | lourdes 951 | lottie 952 | lionel 953 | lenora 954 | laurel 955 | kerri 956 | kelsey 957 | karin 958 | josie 959 | janelle 960 | ismael 961 | helene 962 | gilberto 963 | gale 964 | francisca 965 | fern 966 | etta 967 | estella 968 | elva 969 | effie 970 | dominique 971 | corinne 972 | clint 973 | brittney 974 | aurora 975 | wilfred 976 | tomas 977 | toby 978 | sheldon 979 | santos 980 | maude 981 | lesley 982 | josh 983 | jenifer 984 | iva 985 | ingrid 986 | ina 987 | ignacio 988 | hugo 989 | goldie 990 | eugenia 991 | ervin 992 | erick 993 | elisabeth 994 | dewey 995 | christa 996 | cassie 997 | cary 998 | caleb 999 | caitlin 1000 | bettie 1001 | -------------------------------------------------------------------------------- /examples/gidhistograms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | """Show some histograms for a directory a Xcode project files.""" 10 | 11 | from __future__ import print_function 12 | 13 | import sys 14 | import argparse 15 | from os.path import abspath, dirname, join 16 | import multiprocessing 17 | from collections import defaultdict, Counter 18 | import codecs 19 | 20 | # Set up the Python path so we find the xcodeprojer module in the parent directory 21 | # relative to this file. 22 | sys.path.insert(1, dirname(dirname(abspath(__file__)))) 23 | 24 | import utils 25 | import xcodeprojer 26 | from xcodeprojer import bytestr, unistr 27 | 28 | 29 | PY2 = sys.version_info[0] == 2 30 | PY3 = sys.version_info[0] == 3 31 | 32 | if PY2: 33 | text_type = unicode 34 | binary_type = str 35 | else: 36 | text_type = str 37 | binary_type = bytes 38 | unichr = chr 39 | 40 | 41 | try: 42 | NARROW_BUILD = len(unichr(0x1f300)) == 2 43 | except ValueError: 44 | NARROW_BUILD = True 45 | 46 | 47 | DEFAULT_FIRSTNAMES = 200 48 | 49 | user_hash = xcodeprojer.UniqueXcodeIDGenerator.user_hash 50 | 51 | emojis = [] 52 | 53 | 54 | def here(): 55 | return dirname(abspath(__file__)) 56 | 57 | 58 | def rel(filename): 59 | return join(here(), filename) 60 | 61 | 62 | def write(s, end='\n'): 63 | s = unistr(s) + unistr(end) 64 | s = s.encode('utf-8') 65 | if PY2: 66 | sys.stdout.write(s) 67 | else: 68 | sys.stdout.buffer.write(s) 69 | 70 | 71 | def writeline(): 72 | write('\n') 73 | 74 | 75 | def uniord(s): 76 | """ord that works on surrogate pairs. 77 | """ 78 | try: 79 | return ord(s) 80 | except TypeError: 81 | pass 82 | 83 | if len(s) != 2: 84 | raise 85 | 86 | return 0x10000 + ((ord(s[0]) - 0xd800) << 10) | (ord(s[1]) - 0xdc00) 87 | 88 | 89 | def iterchars(text): 90 | if not NARROW_BUILD: 91 | for c in text: 92 | yield c 93 | 94 | idx = 0 95 | while idx < len(text): 96 | c = text[idx] 97 | if ord(c) >= 0x100: 98 | # When we are running on a narrow Python build 99 | # we have to deal with surrogate pairs ourselves. 100 | if ((0xD800 < ord(c) <= 0xDBFF) 101 | and (idx < len(text) - 1) 102 | and (0xDC00 < ord(text[idx + 1]) <= 0xDFFF)): 103 | c = text[idx:idx+2] 104 | # Skip the other half of the lead and trail surrogate 105 | idx += 1 106 | idx += 1 107 | yield c 108 | 109 | 110 | def build_emoji_table(): 111 | with codecs.open(rel('emojis.txt'), 'r', encoding='utf-8') as f: 112 | text = f.read() 113 | 114 | uniques = set() 115 | for c in iterchars(text): 116 | # Only use unicode chars >= 0x100 (emoji etc.) 117 | if len(c) >= 2 or ord(c) >= 0x100: 118 | if c not in uniques: 119 | emojis.append(c) 120 | uniques.add(c) 121 | 122 | 123 | def print_emoji_table(): 124 | per_line = 32 125 | 126 | for i in range(len(emojis)): 127 | if i % per_line == 0: 128 | write("%3d" % i, end=' ') 129 | write(emojis[i], end=' ') 130 | if i % per_line == per_line - 1: 131 | writeline() 132 | writeline() 133 | 134 | 135 | def print_emoji_histo(histo): 136 | all_users = set() 137 | for year, users in histo.items(): 138 | all_users.update(users) 139 | all_users = sorted(all_users) 140 | num_users = len(all_users) 141 | 142 | for year, users in histo.items(): 143 | chars = [str(year), ' '] 144 | for i in range(num_users): 145 | if all_users[i] in users: 146 | c = emojis[all_users[i]] + ' ' 147 | else: 148 | c = ' ' 149 | chars.append(c) 150 | write(''.join(chars)) 151 | write('\n') 152 | 153 | 154 | def print_histo(histo, utcoffset=0): 155 | maximum = max(histo.values()) 156 | max_display = 60 157 | for k in sorted(histo): 158 | if utcoffset != 0: 159 | localhour = (k - utcoffset) % 24 160 | else: 161 | localhour = k 162 | v = histo.get(localhour, 0) 163 | stars = '*' * int(v * max_display / float(maximum)) 164 | write("%3d %5d %s" % (k, v, stars)) 165 | writeline() 166 | 167 | 168 | def gidtable(filename): 169 | with open(filename, 'rb') as f: 170 | xcodeproj = f.read() 171 | root, parseinfo = xcodeprojer.parse(xcodeproj) 172 | if root is not None: 173 | unparser = xcodeprojer.Unparser(root) 174 | # We don't need the parse tree, only access to the gidcomments 175 | # that are built during the unparse. 176 | _ = unparser.unparse(root, projectname=xcodeprojer.projectname_for_path(filename)) 177 | gidcomments = unparser.gidcomments 178 | c = '.' 179 | else: 180 | gidcomments = {} 181 | c = 'X' 182 | sys.stdout.write(c) 183 | sys.stdout.flush() 184 | return filename, gidcomments 185 | 186 | 187 | def histogram(args, utcoffset=0): 188 | if args.emoji or args.emojitable: 189 | write("Please be patient when your computer is caching emoji fonts for you. This might take a minute.\n") 190 | 191 | build_emoji_table() 192 | if args.emojitable: 193 | print_emoji_table() 194 | return 195 | 196 | path = args.directory 197 | histo_year = Counter() 198 | histo_hour = Counter() 199 | users_per_year = defaultdict(set) 200 | 201 | pool = multiprocessing.Pool(initializer=utils.per_process_init) 202 | 203 | filenames = xcodeprojer.find_projectfiles(path) 204 | results = [] 205 | 206 | write("Looking for Xcode ids in project files...") 207 | sys.stdout.flush() 208 | 209 | for idx, filename in enumerate(filenames): 210 | results.append(pool.apply_async(gidtable, [filename])) 211 | if args.max_files is not None and idx + 1 >= args.max_files: 212 | break 213 | pool.close() 214 | 215 | try: 216 | for asyncresult in results: 217 | filename, gids = asyncresult.get() 218 | for gid in gids: 219 | fields = xcodeprojer.gidfields(gids, gid) 220 | refdate = fields['date'] 221 | dt = xcodeprojer.datetime_from_utc(refdate) 222 | histo_hour[dt.hour] += 1 223 | year = dt.year 224 | if args.startyear <= year <= args.endyear: 225 | histo_year[year] += 1 226 | users_per_year[year].add(fields['user']) 227 | except (KeyboardInterrupt, GeneratorExit): 228 | pool.terminate() 229 | finally: 230 | pool.join() 231 | 232 | writeline() 233 | write("At which hours are new Xcode ids created (UTC time offset: %d)" % args.utcoffset) 234 | print_histo(histo_hour, utcoffset=utcoffset) 235 | 236 | write("In which years were the Xcode ids created (we only look at %s-%s)" % (args.startyear, args.endyear)) 237 | print_histo(histo_year) 238 | 239 | write("Estimated number of users creating new Xcode ids by year") 240 | user_histo = {k: len(v) for (k, v) in users_per_year.items()} 241 | print_histo(user_histo) 242 | 243 | writeline() 244 | write("The following is a list of names that might be completely unrelated to the examined Xcode projects.") 245 | write("For something for tangible replace firstnames.txt with your own list.") 246 | writeline() 247 | 248 | max_firstnames_limited = print_names(args, users_per_year, emoji=args.emoji) 249 | 250 | if args.emoji: 251 | write("Looking for Xcode ids in project files...") 252 | print_emoji_histo(users_per_year) 253 | 254 | if max_firstnames_limited and args.max_firstnames is None: 255 | write("The number of first names to consider was limited to %d, this can be changed with --max-firstnames" % max_firstnames_limited) 256 | 257 | 258 | def print_names(args, users_per_year, emoji=False): 259 | userhashes = defaultdict(list) 260 | max_firstnames = args.max_firstnames 261 | if max_firstnames is None: 262 | max_firstnames = DEFAULT_FIRSTNAMES 263 | max_firstnames_limited = None 264 | with codecs.open(rel('firstnames.txt'), 'r', encoding='utf-8') as f: 265 | firstnames = f.read().splitlines() 266 | for idx, name in enumerate(firstnames): 267 | if idx >= max_firstnames: 268 | max_firstnames_limited = max_firstnames 269 | break 270 | userhashes[user_hash(name)].append(name) 271 | 272 | for year, hashes in sorted(users_per_year.items()): 273 | write(str(year), end=' ') 274 | for h in sorted(hashes): 275 | candidates = userhashes[h] 276 | if candidates: 277 | if emoji: 278 | symbol = emojis[h] + ' ' 279 | else: 280 | symbol = '' 281 | write(' (%s' % symbol + ' | '.join(candidates) + ')', end=' ') 282 | writeline() 283 | 284 | return max_firstnames_limited 285 | 286 | 287 | def main(): 288 | parser = argparse.ArgumentParser(description='Show some histograms for a directory a Xcode project files.') 289 | parser.add_argument('-u', '--utcoffset', type=int, default=-8, metavar='UTCOFFSET', help='UTC time offset, e.g. "-8" for California') 290 | parser.add_argument('--startyear', type=int, default=2006) 291 | parser.add_argument('--endyear', type=int, default=2014) 292 | parser.add_argument('-n', '--max-files', action='store', type=int, default=None, help='maximum number of files to process') 293 | parser.add_argument('--max-firstnames', action='store', type=int, default=None, help='maximum number first names to consider') 294 | parser.add_argument('--emoji', action='store_true', help='add emoji characters to userhashes') 295 | parser.add_argument('--emojitable', action='store_true', help='only print the emoji table') 296 | parser.add_argument('--profile', action='store_true', help='run everything through the profiler') 297 | parser.add_argument('directory', help='directory with Xcode project files') 298 | 299 | args = parser.parse_args() 300 | 301 | if args.profile: 302 | write('Profiling...') 303 | utils.profile('call_command(args, parser)', locals(), globals()) 304 | else: 305 | call_command(args) 306 | 307 | 308 | def call_command(args): 309 | histogram(args, utcoffset=args.utcoffset) 310 | 311 | 312 | if __name__ == '__main__': 313 | main() 314 | -------------------------------------------------------------------------------- /examples/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | import os 10 | import signal 11 | 12 | 13 | def per_process_init(): 14 | os.nice(19) 15 | # A keyboard interrupt disrupts the communication between a 16 | # Python script and its subprocesses when using multiprocessing. 17 | # The child can ignore SIGINT and is properly shut down 18 | # by a pool.terminate() call in case of a keyboard interrupt 19 | # or an early generator exit. 20 | signal.signal(signal.SIGINT, signal.SIG_IGN) 21 | 22 | 23 | def profile(sourcecode, p_locals, p_globals): 24 | import cProfile 25 | import pstats 26 | import tempfile 27 | 28 | prof_filename = os.path.join(tempfile.gettempdir(), "pyapp.prof") 29 | cProfile.runctx(sourcecode, p_locals, p_globals, prof_filename) 30 | 31 | p = pstats.Stats(prof_filename) 32 | p.sort_stats('time').print_stats(40) 33 | os.remove(prof_filename) 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [egg_info] 5 | tag_build = 6 | tag_date = 0 7 | tag_svn_revision = 0 8 | 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup, Command 5 | except ImportError: 6 | from distutils.core import setup, Command 7 | 8 | 9 | class RunTest(Command): 10 | 11 | user_options = [] 12 | 13 | def initialize_options(self): 14 | pass 15 | 16 | def finalize_options(self): 17 | pass 18 | 19 | def run(self): 20 | import sys 21 | import subprocess 22 | errno = subprocess.call([sys.executable, 'tests/test_xcodeprojer.py']) 23 | raise SystemExit(errno) 24 | 25 | 26 | with open('README.rst') as f: 27 | readme = f.read() 28 | 29 | setup( 30 | name='xcodeprojer', 31 | version='0.1', 32 | url='https://github.com/mikr/xcodeprojer', 33 | license='MIT', 34 | author='Michael Krause', 35 | author_email='michael@krause-software.com', 36 | description='xcodeprojer is a Python script that brings your project.pbxproj files in order.', 37 | long_description=readme, 38 | py_modules=['xcodeprojer'], 39 | cmdclass={'test': RunTest}, 40 | zip_safe=False, 41 | platforms='any', 42 | keywords='xcode plist json xml', 43 | classifiers=[ 44 | 'Development Status :: 3 - Alpha', 45 | 'Environment :: Console', 46 | 'Environment :: MacOS X', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: MIT License', 49 | 'Natural Language :: English', 50 | 'Operating System :: MacOS :: MacOS X', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python :: 2.7', 53 | 'Programming Language :: Python :: 3.2', 54 | 'Programming Language :: Python :: 3.3', 55 | 'Programming Language :: Python :: 3.4', 56 | 'Topic :: Software Development :: Libraries :: Python Modules', 57 | 'Topic :: Software Development :: Quality Assurance', 58 | 'Topic :: Utilities', 59 | ], 60 | entry_points={ 61 | 'console_scripts': [ 62 | 'xcodeprojer = xcodeprojer:main' 63 | ] 64 | }, 65 | ) 66 | -------------------------------------------------------------------------------- /tests/check_xcode_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | """ 10 | Check what characters Xcode accepts in quoted or unquoted strings. 11 | 12 | We modify a project.pbxproj repeatedly and run xcodebuild -list 13 | to see if Xcode refuses to parse the modified project. 14 | From the result we get acceptable character ranges. 15 | 16 | The MiniProject is a template that actually contains a test program 17 | that we could actually try to build to see if the project 18 | still makes sense to Xcode after the changes apply. 19 | 20 | Additionally test for various edge cases to see what Xcode 21 | accepts as valid projects to accept or reject them likewise 22 | in our parsers. 23 | """ 24 | 25 | from __future__ import print_function 26 | 27 | import sys 28 | import argparse 29 | import os 30 | import codecs 31 | import tempfile 32 | import subprocess 33 | from os.path import abspath, basename, dirname, join 34 | 35 | 36 | # Set up the Python path so we find the xcodeprojer module in the parent directory 37 | # relative to this file. 38 | sys.path.insert(1, dirname(dirname(abspath(__file__)))) 39 | 40 | import xcodeprojer 41 | from xcodeprojer import bytestr, unistr 42 | 43 | PY2 = sys.version_info[0] == 2 44 | PY3 = sys.version_info[0] == 3 45 | 46 | XCODEBUILD = '/usr/bin/xcodebuild' 47 | MINI_PROJECT_FILENAME = 'data/MiniProject/MiniProject.xcodeproj/project.pbxproj' 48 | 49 | 50 | OK = True 51 | ERR = False 52 | 53 | diverse_variations = [ 54 | [ERR, 'missing_semicolon_terminator', 'SDKROOT = macosx10.9;', 'SDKROOT = macosx10.9'], 55 | [OK, 'missing_comma_terminator', ',\n\t\t\t);', '\n\t\t\t);'], 56 | [OK, 'whitespace_before_utf8_header', '// !$*UTF8*$!', '\n// !$*UTF8*$!'], 57 | [ERR, 'unquoted_unicode_apple', 'ORGANIZATIONNAME = "ACME Inc.";', 'ORGANIZATIONNAME = 🍏;'], 58 | [OK, 'quoted_unicode_apple', 'ORGANIZATIONNAME = "ACME Inc.";', 'ORGANIZATIONNAME = "🍏";'], 59 | ] 60 | 61 | 62 | def rel(filename): 63 | return os.path.join(dirname(abspath(__file__)), filename) 64 | 65 | 66 | def _color(col, text): 67 | if not text: 68 | return text 69 | return '\x1b[0;0;%dm%s\x1b[0;0m' % (col, text) 70 | 71 | 72 | def red(text): 73 | return _color(31, text) 74 | 75 | 76 | def green(text): 77 | return _color(32, text) 78 | 79 | 80 | def xcodebuild_list(pbxfilename): 81 | xcodeargs = [XCODEBUILD, '-list', '-project', dirname(pbxfilename)] 82 | client = subprocess.Popen(xcodeargs, 83 | stdin=subprocess.PIPE, 84 | stdout=subprocess.PIPE, 85 | stderr=subprocess.PIPE) 86 | stdout, stderr = client.communicate() 87 | return client.returncode, stdout, stderr 88 | 89 | 90 | def list_project(pbxfilename, data): 91 | with codecs.open(pbxfilename, 'w', encoding='utf-8') as f: 92 | f.write(data) 93 | return xcodebuild_list(pbxfilename) 94 | 95 | 96 | def safe_replace(text, a, b): 97 | a = unistr(a) 98 | b = unistr(b) 99 | result = text.replace(a, b) 100 | if result == text: 101 | raise ValueError("Error: replacing '%s' with '%s' did not change the text." % (a, b)) 102 | return result 103 | 104 | 105 | def report(text, a, b, ret, expected): 106 | if expected == (ret == 0): 107 | exptext = green('as expected: ') 108 | else: 109 | exptext = red(' unexpected: ') 110 | if ret == 0: 111 | print(exptext + " OK : " + green(text)) 112 | bcol = green 113 | else: 114 | print(exptext + "ERR : " + red(text)) 115 | bcol = red 116 | print(" when we replace '" + green(a) + "'") 117 | print(" with '" + bcol(b) + "'") 118 | 119 | 120 | def parsable_variation(pbxdata, expected, desc, a, b): 121 | pbxfilename, data = pbxdata 122 | if sys.platform == 'darwin' and os.path.exists(XCODEBUILD): 123 | ret, stdout, stderr = list_project(pbxfilename, safe_replace(data, a, b)) 124 | else: 125 | print("Skipping 'xcodebuild -list' because we don't have it") 126 | ret, stdout, stderr = not expected, '', '' 127 | report(desc, a, b, ret, expected) 128 | return ret 129 | 130 | 131 | def check_all_parsers(pbxdata, expected, desc, a, b): 132 | pbxfilename, data = pbxdata 133 | ret = parsable_variation(pbxdata, expected, desc, a, b) 134 | 135 | moddata = safe_replace(data, a, b) 136 | for parsertype in 'fast', 'classic': 137 | root, parseinfo = xcodeprojer.parse(moddata, format='xcode', 138 | parsertype=parsertype) 139 | if ret == 0: 140 | # Xcode parsed this. So if our parsers have anything to report, now is the time. 141 | xcodeprojer.report_parse_status(root, parseinfo, filename=pbxfilename) 142 | if root is None: 143 | print("Error: parsertype %s failed where Xcode succeeded:\n %s: %r %r" % (parsertype, desc, a, b)) 144 | else: 145 | if root is not None: 146 | print("Warning: parsertype %s succeeded where Xcode failed:\n %s: %r %r" % (parsertype, desc, a, b)) 147 | flush() 148 | 149 | 150 | def run_diverse(pbxfilename, data): 151 | pbxdata = (pbxfilename, data) 152 | 153 | for expected, desc, a, b in diverse_variations: 154 | check_all_parsers(pbxdata, expected, desc, a, b) 155 | 156 | 157 | def flush(): 158 | sys.stdout.flush() 159 | sys.stderr.flush() 160 | 161 | 162 | def rangefor(args): 163 | start = args.firstchar or 0 164 | end = args.lastchar 165 | if end is None: 166 | end = 127 167 | end = min(end, 127) 168 | 169 | for i in range(start, end + 1): 170 | c = unichr(i) 171 | if (not args.alnums) and c.isalnum(): 172 | # We usually don't check for [0-9a-zA-Z] 173 | continue 174 | yield i, c 175 | 176 | 177 | def run_xcodebuild(args, pbxfilename, data): 178 | srctext = 'developmentRegion = English;' 179 | parts = 'developmentRegion = Eng', 'lish;' 180 | 181 | validchars = set() 182 | invalidchars = set() 183 | for i, c in rangefor(args): 184 | desttext = parts[0] + c + parts[1] 185 | destdata = safe_replace(data, srctext, desttext) 186 | 187 | ret, stdout, stderr = list_project(pbxfilename, destdata) 188 | if ret == 0: 189 | theset = validchars 190 | col = green 191 | else: 192 | theset = invalidchars 193 | col = red 194 | 195 | theset.add(c) 196 | print(col("(%d %r)" % (i, c)), end=' ') 197 | flush() 198 | 199 | print() 200 | print(" valid: '%s'" % green(''.join(sorted(validchars)))) 201 | print("invalid: '%s'" % red(''.join(sorted(invalidchars)))) 202 | 203 | 204 | def run_selected_checks(args, parser, projdata, destpbxfilename): 205 | if args.unquoted: 206 | run_xcodebuild(args, destpbxfilename, projdata) 207 | elif args.diverse: 208 | run_diverse(destpbxfilename, projdata) 209 | else: 210 | parser.error('Something is wrong with the options or the handling of them.') 211 | 212 | 213 | def run_checks(args, parser): 214 | print("Starting Xcode checks, meaning we run xcodebuild repeatedly with almost the same project.") 215 | 216 | pbxfilename = rel(MINI_PROJECT_FILENAME) 217 | 218 | with codecs.open(pbxfilename, 'r', encoding='utf-8') as f: 219 | projdata = f.read() 220 | 221 | xcproj = basename(dirname(pbxfilename)) 222 | tmprootdir = tempfile.mkdtemp(prefix=xcodeprojer.timestamp()) 223 | destpbxfilename = join(tmprootdir, xcproj, basename(pbxfilename)) 224 | try: 225 | os.makedirs(dirname(destpbxfilename)) 226 | run_selected_checks(args, parser, projdata, destpbxfilename) 227 | finally: 228 | try: 229 | os.remove(destpbxfilename) 230 | except OSError: 231 | pass 232 | for d in [dirname(destpbxfilename), tmprootdir]: 233 | try: 234 | os.remove(d) 235 | except OSError: 236 | pass 237 | 238 | 239 | def main(): 240 | parser = argparse.ArgumentParser(description='Find and test local project files.') 241 | 242 | actions = 'unquoted diverse'.split() 243 | parser.add_argument('--unquoted', action='store_true', help='which characters does Xcode accept without double quotes') 244 | parser.add_argument('--diverse', action='store_true', help='tests various behaviours') 245 | parser.add_argument('-f', '--first', action='store', type=int, dest='firstchar', help='ASCII code of first character') 246 | parser.add_argument('-l', '--last', action='store', type=int, dest='lastchar', help='ASCII code of last character') 247 | parser.add_argument('--alnums', action='store_true', help='also process alnums [a-zA-Z0-9]') 248 | 249 | args = parser.parse_args() 250 | 251 | num_actions = 0 252 | for act in actions: 253 | if getattr(args, act): 254 | num_actions += 1 255 | 256 | if num_actions != 1: 257 | parser.error('Please specify exactly one of the options %s.' % ', '.join('--' + x for x in actions)) 258 | 259 | run_checks(args, parser) 260 | 261 | 262 | if __name__ == '__main__': 263 | if PY3: 264 | sys.stdout = codecs.getwriter('utf8')(sys.stdout.buffer) 265 | sys.stderr = codecs.getwriter('utf8')(sys.stderr.buffer) 266 | main() 267 | -------------------------------------------------------------------------------- /tests/data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "archiveVersion":"1", 3 | "classes":{}, 4 | "objectVersion":"46", 5 | "objects":{ 6 | "4C36A8C319A0D91D00F6C76D":{ 7 | "children":[ 8 | "4C36A8CE19A0D91D00F6C76D", 9 | "4C36A8CD19A0D91D00F6C76D" 10 | ], 11 | "isa":"PBXGroup", 12 | "sourceTree":"" 13 | }, 14 | "4C36A8C419A0D91D00F6C76D":{ 15 | "attributes":{ 16 | "LastUpgradeCheck":"0510", 17 | "ORGANIZATIONNAME":"\ud83c\udfab" 18 | }, 19 | "buildConfigurationList":"4C36A8C719A0D91D00F6C76D", 20 | "compatibilityVersion":"Xcode 3.2", 21 | "developmentRegion":"English", 22 | "hasScannedForEncodings":"0", 23 | "isa":"PBXProject", 24 | "knownRegions":[ 25 | "en" 26 | ], 27 | "mainGroup":"4C36A8C319A0D91D00F6C76D", 28 | "productRefGroup":"4C36A8CD19A0D91D00F6C76D", 29 | "projectDirPath":"", 30 | "projectRoot":"", 31 | "targets":[ 32 | "4C36A8CB19A0D91D00F6C76D" 33 | ] 34 | }, 35 | "4C36A8C719A0D91D00F6C76D":{ 36 | "buildConfigurations":[ 37 | "4C36A8D319A0D91D00F6C76D", 38 | "4C36A8D419A0D91D00F6C76D" 39 | ], 40 | "defaultConfigurationIsVisible":"0", 41 | "defaultConfigurationName":"Release", 42 | "isa":"XCConfigurationList" 43 | }, 44 | "4C36A8C819A0D91D00F6C76D":{ 45 | "buildActionMask":"2147483647", 46 | "files":[ 47 | "4C36A8D019A0D91D00F6C76D" 48 | ], 49 | "isa":"PBXSourcesBuildPhase", 50 | "runOnlyForDeploymentPostprocessing":"0" 51 | }, 52 | "4C36A8C919A0D91D00F6C76D":{ 53 | "buildActionMask":"2147483647", 54 | "files":[], 55 | "isa":"PBXFrameworksBuildPhase", 56 | "runOnlyForDeploymentPostprocessing":"0" 57 | }, 58 | "4C36A8CA19A0D91D00F6C76D":{ 59 | "buildActionMask":"2147483647", 60 | "dstPath":"/usr/share/man/man1/", 61 | "dstSubfolderSpec":"0", 62 | "files":[], 63 | "isa":"PBXCopyFilesBuildPhase", 64 | "runOnlyForDeploymentPostprocessing":"1" 65 | }, 66 | "4C36A8CB19A0D91D00F6C76D":{ 67 | "buildConfigurationList":"4C36A8D519A0D91D00F6C76D", 68 | "buildPhases":[ 69 | "4C6D58B919A128F1004CF31A", 70 | "4C36A8C819A0D91D00F6C76D", 71 | "4C36A8C919A0D91D00F6C76D", 72 | "4C36A8CA19A0D91D00F6C76D" 73 | ], 74 | "buildRules":[], 75 | "dependencies":[], 76 | "isa":"PBXNativeTarget", 77 | "name":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct", 78 | "productName":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct", 79 | "productReference":"4C36A8CC19A0D91D00F6C76D", 80 | "productType":"com.apple.product-type.tool" 81 | }, 82 | "4C36A8CC19A0D91D00F6C76D":{ 83 | "explicitFileType":"compiled.mach-o.executable", 84 | "includeInIndex":"0", 85 | "isa":"PBXFileReference", 86 | "path":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct", 87 | "sourceTree":"BUILT_PRODUCTS_DIR" 88 | }, 89 | "4C36A8CD19A0D91D00F6C76D":{ 90 | "children":[ 91 | "4C36A8CC19A0D91D00F6C76D" 92 | ], 93 | "isa":"PBXGroup", 94 | "name":"Products", 95 | "sourceTree":"" 96 | }, 97 | "4C36A8CE19A0D91D00F6C76D":{ 98 | "children":[ 99 | "4C36A8CF19A0D91D00F6C76D" 100 | ], 101 | "isa":"PBXGroup", 102 | "path":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct", 103 | "sourceTree":"" 104 | }, 105 | "4C36A8CF19A0D91D00F6C76D":{ 106 | "isa":"PBXFileReference", 107 | "lastKnownFileType":"sourcecode.c.c", 108 | "path":"main.c", 109 | "sourceTree":"" 110 | }, 111 | "4C36A8D019A0D91D00F6C76D":{ 112 | "fileRef":"4C36A8CF19A0D91D00F6C76D", 113 | "isa":"PBXBuildFile" 114 | }, 115 | "4C36A8D319A0D91D00F6C76D":{ 116 | "buildSettings":{ 117 | "ALWAYS_SEARCH_USER_PATHS":"NO", 118 | "COPY_PHASE_STRIP":"NO", 119 | "GCC_PREPROCESSOR_DEFINITIONS":[ 120 | "DEBUG=1", 121 | "$(inherited)" 122 | ], 123 | "MACOSX_DEPLOYMENT_TARGET":"10.9", 124 | "ONLY_ACTIVE_ARCH":"YES", 125 | "SDKROOT":"macosx" 126 | }, 127 | "isa":"XCBuildConfiguration", 128 | "name":"Debug" 129 | }, 130 | "4C36A8D419A0D91D00F6C76D":{ 131 | "buildSettings":{ 132 | "ALWAYS_SEARCH_USER_PATHS":"NO", 133 | "COPY_PHASE_STRIP":"YES", 134 | "MACOSX_DEPLOYMENT_TARGET":"10.9", 135 | "SDKROOT":"macosx" 136 | }, 137 | "isa":"XCBuildConfiguration", 138 | "name":"Release" 139 | }, 140 | "4C36A8D519A0D91D00F6C76D":{ 141 | "buildConfigurations":[ 142 | "4C36A8D619A0D91D00F6C76D", 143 | "4C36A8D719A0D91D00F6C76D" 144 | ], 145 | "defaultConfigurationIsVisible":"0", 146 | "defaultConfigurationName":"Release", 147 | "isa":"XCConfigurationList" 148 | }, 149 | "4C36A8D619A0D91D00F6C76D":{ 150 | "buildSettings":{ 151 | "PRODUCT_NAME":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct" 152 | }, 153 | "isa":"XCBuildConfiguration", 154 | "name":"Debug" 155 | }, 156 | "4C36A8D719A0D91D00F6C76D":{ 157 | "buildSettings":{ 158 | "PRODUCT_NAME":"In\u0303te\u0308rna\u0302tio\u0302na\u0300l Xco\u0308de\u0300 Pr\u00f8j\u00e6ct" 159 | }, 160 | "isa":"XCBuildConfiguration", 161 | "name":"Release" 162 | }, 163 | "4C6D58B919A128F1004CF31A":{ 164 | "buildActionMask":"2147483647", 165 | "files":[], 166 | "inputPaths":[], 167 | "isa":"PBXShellScriptBuildPhase", 168 | "outputPaths":[], 169 | "runOnlyForDeploymentPostprocessing":"0", 170 | "shellPath":"/bin/sh", 171 | "shellScript":"echo \"Here is a run script buildphase\\nright after the \\\\target dependencies/\"\necho \"I\u00f1t\u00ebrn\u00e2ti\u00f4n\u00e0liz\u00e6ti\u00f8n ready?\"\necho \"& XML as well ?\"\n" 172 | } 173 | }, 174 | "rootObject":"4C36A8C419A0D91D00F6C76D" 175 | } -------------------------------------------------------------------------------- /tests/data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4C36A8D019A0D91D00F6C76D /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C36A8CF19A0D91D00F6C76D /* main.c */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 4C36A8CA19A0D91D00F6C76D /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 4C36A8CC19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "Iñtërnâtiônàl Xcödè Prøjæct"; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 4C36A8CF19A0D91D00F6C76D /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 4C36A8C919A0D91D00F6C76D /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 4C36A8C319A0D91D00F6C76D = { 42 | isa = PBXGroup; 43 | children = ( 44 | 4C36A8CE19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */, 45 | 4C36A8CD19A0D91D00F6C76D /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 4C36A8CD19A0D91D00F6C76D /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 4C36A8CC19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 4C36A8CE19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 4C36A8CF19A0D91D00F6C76D /* main.c */, 61 | ); 62 | path = "Iñtërnâtiônàl Xcödè Prøjæct"; 63 | sourceTree = ""; 64 | }; 65 | /* End PBXGroup section */ 66 | 67 | /* Begin PBXNativeTarget section */ 68 | 4C36A8CB19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */ = { 69 | isa = PBXNativeTarget; 70 | buildConfigurationList = 4C36A8D519A0D91D00F6C76D /* Build configuration list for PBXNativeTarget "Iñtërnâtiônàl Xcödè Prøjæct" */; 71 | buildPhases = ( 72 | 4C6D58B919A128F1004CF31A /* ShellScript */, 73 | 4C36A8C819A0D91D00F6C76D /* Sources */, 74 | 4C36A8C919A0D91D00F6C76D /* Frameworks */, 75 | 4C36A8CA19A0D91D00F6C76D /* CopyFiles */, 76 | ); 77 | buildRules = ( 78 | ); 79 | dependencies = ( 80 | ); 81 | name = "Iñtërnâtiônàl Xcödè Prøjæct"; 82 | productName = "Iñtërnâtiônàl Xcödè Prøjæct"; 83 | productReference = 4C36A8CC19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */; 84 | productType = "com.apple.product-type.tool"; 85 | }; 86 | /* End PBXNativeTarget section */ 87 | 88 | /* Begin PBXProject section */ 89 | 4C36A8C419A0D91D00F6C76D /* Project object */ = { 90 | isa = PBXProject; 91 | attributes = { 92 | LastUpgradeCheck = 0510; 93 | ORGANIZATIONNAME = "🎫"; 94 | }; 95 | buildConfigurationList = 4C36A8C719A0D91D00F6C76D /* Build configuration list for PBXProject "IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt" */; 96 | compatibilityVersion = "Xcode 3.2"; 97 | developmentRegion = English; 98 | hasScannedForEncodings = 0; 99 | knownRegions = ( 100 | en, 101 | ); 102 | mainGroup = 4C36A8C319A0D91D00F6C76D; 103 | productRefGroup = 4C36A8CD19A0D91D00F6C76D /* Products */; 104 | projectDirPath = ""; 105 | projectRoot = ""; 106 | targets = ( 107 | 4C36A8CB19A0D91D00F6C76D /* Iñtërnâtiônàl Xcödè Prøjæct */, 108 | ); 109 | }; 110 | /* End PBXProject section */ 111 | 112 | /* Begin PBXShellScriptBuildPhase section */ 113 | 4C6D58B919A128F1004CF31A /* ShellScript */ = { 114 | isa = PBXShellScriptBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | ); 118 | inputPaths = ( 119 | ); 120 | outputPaths = ( 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | shellPath = /bin/sh; 124 | shellScript = "echo \"Here is a run script buildphase\\nright after the \\\\target dependencies/\"\necho \"Iñtërnâtiônàlizætiøn ready?\"\necho \"& XML as well ?\"\n"; 125 | }; 126 | /* End PBXShellScriptBuildPhase section */ 127 | 128 | /* Begin PBXSourcesBuildPhase section */ 129 | 4C36A8C819A0D91D00F6C76D /* Sources */ = { 130 | isa = PBXSourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | 4C36A8D019A0D91D00F6C76D /* main.c in Sources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXSourcesBuildPhase section */ 138 | 139 | /* Begin XCBuildConfiguration section */ 140 | 4C36A8D319A0D91D00F6C76D /* Debug */ = { 141 | isa = XCBuildConfiguration; 142 | buildSettings = { 143 | ALWAYS_SEARCH_USER_PATHS = NO; 144 | COPY_PHASE_STRIP = NO; 145 | GCC_PREPROCESSOR_DEFINITIONS = ( 146 | "DEBUG=1", 147 | "$(inherited)", 148 | ); 149 | MACOSX_DEPLOYMENT_TARGET = 10.9; 150 | ONLY_ACTIVE_ARCH = YES; 151 | SDKROOT = macosx; 152 | }; 153 | name = Debug; 154 | }; 155 | 4C36A8D419A0D91D00F6C76D /* Release */ = { 156 | isa = XCBuildConfiguration; 157 | buildSettings = { 158 | ALWAYS_SEARCH_USER_PATHS = NO; 159 | COPY_PHASE_STRIP = YES; 160 | MACOSX_DEPLOYMENT_TARGET = 10.9; 161 | SDKROOT = macosx; 162 | }; 163 | name = Release; 164 | }; 165 | 4C36A8D619A0D91D00F6C76D /* Debug */ = { 166 | isa = XCBuildConfiguration; 167 | buildSettings = { 168 | PRODUCT_NAME = "Iñtërnâtiônàl Xcödè Prøjæct"; 169 | }; 170 | name = Debug; 171 | }; 172 | 4C36A8D719A0D91D00F6C76D /* Release */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | PRODUCT_NAME = "Iñtërnâtiônàl Xcödè Prøjæct"; 176 | }; 177 | name = Release; 178 | }; 179 | /* End XCBuildConfiguration section */ 180 | 181 | /* Begin XCConfigurationList section */ 182 | 4C36A8C719A0D91D00F6C76D /* Build configuration list for PBXProject "IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt" */ = { 183 | isa = XCConfigurationList; 184 | buildConfigurations = ( 185 | 4C36A8D319A0D91D00F6C76D /* Debug */, 186 | 4C36A8D419A0D91D00F6C76D /* Release */, 187 | ); 188 | defaultConfigurationIsVisible = 0; 189 | defaultConfigurationName = Release; 190 | }; 191 | 4C36A8D519A0D91D00F6C76D /* Build configuration list for PBXNativeTarget "Iñtërnâtiônàl Xcödè Prøjæct" */ = { 192 | isa = XCConfigurationList; 193 | buildConfigurations = ( 194 | 4C36A8D619A0D91D00F6C76D /* Debug */, 195 | 4C36A8D719A0D91D00F6C76D /* Release */, 196 | ); 197 | defaultConfigurationIsVisible = 0; 198 | defaultConfigurationName = Release; 199 | }; 200 | /* End XCConfigurationList section */ 201 | }; 202 | rootObject = 4C36A8C419A0D91D00F6C76D /* Project object */; 203 | } 204 | -------------------------------------------------------------------------------- /tests/data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | archiveVersion 6 | 1 7 | classes 8 | 9 | objectVersion 10 | 46 11 | objects 12 | 13 | 4C36A8C319A0D91D00F6C76D 14 | 15 | children 16 | 17 | 4C36A8CE19A0D91D00F6C76D 18 | 4C36A8CD19A0D91D00F6C76D 19 | 20 | isa 21 | PBXGroup 22 | sourceTree 23 | <group> 24 | 25 | 4C36A8C419A0D91D00F6C76D 26 | 27 | attributes 28 | 29 | LastUpgradeCheck 30 | 0510 31 | ORGANIZATIONNAME 32 | 🎫 33 | 34 | buildConfigurationList 35 | 4C36A8C719A0D91D00F6C76D 36 | compatibilityVersion 37 | Xcode 3.2 38 | developmentRegion 39 | English 40 | hasScannedForEncodings 41 | 0 42 | isa 43 | PBXProject 44 | knownRegions 45 | 46 | en 47 | 48 | mainGroup 49 | 4C36A8C319A0D91D00F6C76D 50 | productRefGroup 51 | 4C36A8CD19A0D91D00F6C76D 52 | projectDirPath 53 | 54 | projectRoot 55 | 56 | targets 57 | 58 | 4C36A8CB19A0D91D00F6C76D 59 | 60 | 61 | 4C36A8C719A0D91D00F6C76D 62 | 63 | buildConfigurations 64 | 65 | 4C36A8D319A0D91D00F6C76D 66 | 4C36A8D419A0D91D00F6C76D 67 | 68 | defaultConfigurationIsVisible 69 | 0 70 | defaultConfigurationName 71 | Release 72 | isa 73 | XCConfigurationList 74 | 75 | 4C36A8C819A0D91D00F6C76D 76 | 77 | buildActionMask 78 | 2147483647 79 | files 80 | 81 | 4C36A8D019A0D91D00F6C76D 82 | 83 | isa 84 | PBXSourcesBuildPhase 85 | runOnlyForDeploymentPostprocessing 86 | 0 87 | 88 | 4C36A8C919A0D91D00F6C76D 89 | 90 | buildActionMask 91 | 2147483647 92 | files 93 | 94 | isa 95 | PBXFrameworksBuildPhase 96 | runOnlyForDeploymentPostprocessing 97 | 0 98 | 99 | 4C36A8CA19A0D91D00F6C76D 100 | 101 | buildActionMask 102 | 2147483647 103 | dstPath 104 | /usr/share/man/man1/ 105 | dstSubfolderSpec 106 | 0 107 | files 108 | 109 | isa 110 | PBXCopyFilesBuildPhase 111 | runOnlyForDeploymentPostprocessing 112 | 1 113 | 114 | 4C36A8CB19A0D91D00F6C76D 115 | 116 | buildConfigurationList 117 | 4C36A8D519A0D91D00F6C76D 118 | buildPhases 119 | 120 | 4C6D58B919A128F1004CF31A 121 | 4C36A8C819A0D91D00F6C76D 122 | 4C36A8C919A0D91D00F6C76D 123 | 4C36A8CA19A0D91D00F6C76D 124 | 125 | buildRules 126 | 127 | dependencies 128 | 129 | isa 130 | PBXNativeTarget 131 | name 132 | Iñtërnâtiônàl Xcödè Prøjæct 133 | productName 134 | Iñtërnâtiônàl Xcödè Prøjæct 135 | productReference 136 | 4C36A8CC19A0D91D00F6C76D 137 | productType 138 | com.apple.product-type.tool 139 | 140 | 4C36A8CC19A0D91D00F6C76D 141 | 142 | explicitFileType 143 | compiled.mach-o.executable 144 | includeInIndex 145 | 0 146 | isa 147 | PBXFileReference 148 | path 149 | Iñtërnâtiônàl Xcödè Prøjæct 150 | sourceTree 151 | BUILT_PRODUCTS_DIR 152 | 153 | 4C36A8CD19A0D91D00F6C76D 154 | 155 | children 156 | 157 | 4C36A8CC19A0D91D00F6C76D 158 | 159 | isa 160 | PBXGroup 161 | name 162 | Products 163 | sourceTree 164 | <group> 165 | 166 | 4C36A8CE19A0D91D00F6C76D 167 | 168 | children 169 | 170 | 4C36A8CF19A0D91D00F6C76D 171 | 172 | isa 173 | PBXGroup 174 | path 175 | Iñtërnâtiônàl Xcödè Prøjæct 176 | sourceTree 177 | <group> 178 | 179 | 4C36A8CF19A0D91D00F6C76D 180 | 181 | isa 182 | PBXFileReference 183 | lastKnownFileType 184 | sourcecode.c.c 185 | path 186 | main.c 187 | sourceTree 188 | <group> 189 | 190 | 4C36A8D019A0D91D00F6C76D 191 | 192 | fileRef 193 | 4C36A8CF19A0D91D00F6C76D 194 | isa 195 | PBXBuildFile 196 | 197 | 4C36A8D319A0D91D00F6C76D 198 | 199 | buildSettings 200 | 201 | ALWAYS_SEARCH_USER_PATHS 202 | NO 203 | COPY_PHASE_STRIP 204 | NO 205 | GCC_PREPROCESSOR_DEFINITIONS 206 | 207 | DEBUG=1 208 | $(inherited) 209 | 210 | MACOSX_DEPLOYMENT_TARGET 211 | 10.9 212 | ONLY_ACTIVE_ARCH 213 | YES 214 | SDKROOT 215 | macosx 216 | 217 | isa 218 | XCBuildConfiguration 219 | name 220 | Debug 221 | 222 | 4C36A8D419A0D91D00F6C76D 223 | 224 | buildSettings 225 | 226 | ALWAYS_SEARCH_USER_PATHS 227 | NO 228 | COPY_PHASE_STRIP 229 | YES 230 | MACOSX_DEPLOYMENT_TARGET 231 | 10.9 232 | SDKROOT 233 | macosx 234 | 235 | isa 236 | XCBuildConfiguration 237 | name 238 | Release 239 | 240 | 4C36A8D519A0D91D00F6C76D 241 | 242 | buildConfigurations 243 | 244 | 4C36A8D619A0D91D00F6C76D 245 | 4C36A8D719A0D91D00F6C76D 246 | 247 | defaultConfigurationIsVisible 248 | 0 249 | defaultConfigurationName 250 | Release 251 | isa 252 | XCConfigurationList 253 | 254 | 4C36A8D619A0D91D00F6C76D 255 | 256 | buildSettings 257 | 258 | PRODUCT_NAME 259 | Iñtërnâtiônàl Xcödè Prøjæct 260 | 261 | isa 262 | XCBuildConfiguration 263 | name 264 | Debug 265 | 266 | 4C36A8D719A0D91D00F6C76D 267 | 268 | buildSettings 269 | 270 | PRODUCT_NAME 271 | Iñtërnâtiônàl Xcödè Prøjæct 272 | 273 | isa 274 | XCBuildConfiguration 275 | name 276 | Release 277 | 278 | 4C6D58B919A128F1004CF31A 279 | 280 | buildActionMask 281 | 2147483647 282 | files 283 | 284 | inputPaths 285 | 286 | isa 287 | PBXShellScriptBuildPhase 288 | outputPaths 289 | 290 | runOnlyForDeploymentPostprocessing 291 | 0 292 | shellPath 293 | /bin/sh 294 | shellScript 295 | echo "Here is a run script buildphase\nright after the \\target dependencies/" 296 | echo "Iñtërnâtiônàlizætiøn ready?" 297 | echo "& XML <ready> as well ?" 298 | 299 | 300 | 301 | rootObject 302 | 4C36A8C419A0D91D00F6C76D 303 | 304 | 305 | -------------------------------------------------------------------------------- /tests/data/MiniProject/MiniProject.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4CDE96A619B3613C009DF310 /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE96A519B3613C009DF310 /* main.c */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 4CDE96A019B3613C009DF310 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 4CDE96A219B3613C009DF310 /* MiniProject */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = MiniProject; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 4CDE96A519B3613C009DF310 /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 4CDE969F19B3613C009DF310 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 4CDE969919B3613C009DF310 = { 42 | isa = PBXGroup; 43 | children = ( 44 | 4CDE96A419B3613C009DF310 /* MiniProject */, 45 | 4CDE96A319B3613C009DF310 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 4CDE96A319B3613C009DF310 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 4CDE96A219B3613C009DF310 /* MiniProject */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 4CDE96A419B3613C009DF310 /* MiniProject */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 4CDE96A519B3613C009DF310 /* main.c */, 61 | ); 62 | path = MiniProject; 63 | sourceTree = ""; 64 | }; 65 | /* End PBXGroup section */ 66 | 67 | /* Begin PBXNativeTarget section */ 68 | 4CDE96A119B3613C009DF310 /* MiniProject */ = { 69 | isa = PBXNativeTarget; 70 | buildConfigurationList = 4CDE96A919B3613C009DF310 /* Build configuration list for PBXNativeTarget "MiniProject" */; 71 | buildPhases = ( 72 | 4CDE969E19B3613C009DF310 /* Sources */, 73 | 4CDE969F19B3613C009DF310 /* Frameworks */, 74 | 4CDE96A019B3613C009DF310 /* CopyFiles */, 75 | ); 76 | buildRules = ( 77 | ); 78 | dependencies = ( 79 | ); 80 | name = MiniProject; 81 | productName = MiniProject; 82 | productReference = 4CDE96A219B3613C009DF310 /* MiniProject */; 83 | productType = "com.apple.product-type.tool"; 84 | }; 85 | /* End PBXNativeTarget section */ 86 | 87 | /* Begin PBXProject section */ 88 | 4CDE969A19B3613C009DF310 /* Project object */ = { 89 | isa = PBXProject; 90 | attributes = { 91 | LastUpgradeCheck = 0600; 92 | ORGANIZATIONNAME = "ACME Inc."; 93 | TargetAttributes = { 94 | 4CDE96A119B3613C009DF310 = { 95 | CreatedOnToolsVersion = 6.0; 96 | }; 97 | }; 98 | }; 99 | buildConfigurationList = 4CDE969D19B3613C009DF310 /* Build configuration list for PBXProject "MiniProject" */; 100 | compatibilityVersion = "Xcode 3.2"; 101 | developmentRegion = English; 102 | hasScannedForEncodings = 0; 103 | knownRegions = ( 104 | en, 105 | ); 106 | mainGroup = 4CDE969919B3613C009DF310; 107 | productRefGroup = 4CDE96A319B3613C009DF310 /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | 4CDE96A119B3613C009DF310 /* MiniProject */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXSourcesBuildPhase section */ 117 | 4CDE969E19B3613C009DF310 /* Sources */ = { 118 | isa = PBXSourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | 4CDE96A619B3613C009DF310 /* main.c in Sources */, 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXSourcesBuildPhase section */ 126 | 127 | /* Begin XCBuildConfiguration section */ 128 | 4CDE96A719B3613C009DF310 /* Debug */ = { 129 | isa = XCBuildConfiguration; 130 | buildSettings = { 131 | ALWAYS_SEARCH_USER_PATHS = NO; 132 | GCC_PREPROCESSOR_DEFINITIONS = ( 133 | "DEBUG=1", 134 | "$(inherited)", 135 | ); 136 | }; 137 | name = Debug; 138 | }; 139 | 4CDE96AA19B3613C009DF310 /* Debug */ = { 140 | isa = XCBuildConfiguration; 141 | buildSettings = { 142 | PRODUCT_NAME = "$(TARGET_NAME)"; 143 | SDKROOT = macosx10.9; 144 | }; 145 | name = Debug; 146 | }; 147 | /* End XCBuildConfiguration section */ 148 | 149 | /* Begin XCConfigurationList section */ 150 | 4CDE969D19B3613C009DF310 /* Build configuration list for PBXProject "MiniProject" */ = { 151 | isa = XCConfigurationList; 152 | buildConfigurations = ( 153 | 4CDE96A719B3613C009DF310 /* Debug */, 154 | ); 155 | defaultConfigurationIsVisible = 0; 156 | defaultConfigurationName = Debug; 157 | }; 158 | 4CDE96A919B3613C009DF310 /* Build configuration list for PBXNativeTarget "MiniProject" */ = { 159 | isa = XCConfigurationList; 160 | buildConfigurations = ( 161 | 4CDE96AA19B3613C009DF310 /* Debug */, 162 | ); 163 | defaultConfigurationIsVisible = 0; 164 | defaultConfigurationName = Debug; 165 | }; 166 | /* End XCConfigurationList section */ 167 | }; 168 | rootObject = 4CDE969A19B3613C009DF310 /* Project object */; 169 | } 170 | -------------------------------------------------------------------------------- /tests/test_xcodeprojer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | """Tests for xcodeprojer.""" 10 | 11 | from __future__ import print_function 12 | 13 | import sys 14 | import unittest 15 | import datetime 16 | import subprocess 17 | import os 18 | import glob 19 | from os.path import abspath, dirname, splitext 20 | import calendar as cal 21 | import re 22 | import argparse 23 | from io import StringIO 24 | import json 25 | 26 | # Set up the Python path so we find the xcodeprojer module in the parent directory 27 | # relative to this file. 28 | sys.path.insert(1, dirname(dirname(abspath(__file__)))) 29 | 30 | import xcodeprojer 31 | from xcodeprojer import bytestr, unistr 32 | 33 | u = unistr 34 | 35 | PY2 = sys.version_info[0] == 2 36 | PY3 = sys.version_info[0] == 3 37 | 38 | 39 | if PY2: 40 | text_type = unicode 41 | binary_type = str 42 | else: 43 | text_type = str 44 | 45 | 46 | TEST_AGAINST_PLUTIL = False 47 | 48 | # The unicode tests should be as thorough as possible but if we'd 49 | # use characters which have a composed as well as a decomposed unicode form 50 | # the relationship between our filesystem and our development tools (notably git) 51 | # becomes strained to a point that even running a diff becomes non-trivial. 52 | # We rather choose a clean git repository over ambiguous unicode representations, 53 | # therefore no umlauts etc. in the filenames. 54 | INTL_PROJECT_FILENAME = 'data/IŋƫƐrnætiønæl X©ødǝ ¶®øjæƈt.xcodeproj/project.pbxproj' 55 | MINI_PROJECT_FILENAME = 'data/MiniProject/MiniProject.xcodeproj/project.pbxproj' 56 | 57 | 58 | PROJECT_TEMPLATE = """// !$*UTF8*$! 59 | {%(toplevel)s 60 | \tobjectVersion = %(objectVersion)s; 61 | \tobjects = { 62 | \t%(objects)s}; 63 | } 64 | """ 65 | 66 | re_unisubst = re.compile('[^\x00-\x7f]') 67 | re_uniglob = re.compile(r'\*+') 68 | 69 | 70 | def unifilename(filename): 71 | if isinstance(filename, text_type) or PY3: 72 | return filename 73 | try: 74 | return filename.decode(sys.getfilesystemencoding()) 75 | except UnicodeEncodeError: 76 | return filename.decode('utf-8') 77 | 78 | 79 | def sysfilename(filename): 80 | if not isinstance(filename, text_type) or PY3: 81 | return filename 82 | try: 83 | return filename.encode(sys.getfilesystemencoding()) 84 | except UnicodeEncodeError: 85 | return filename.encode('utf-8') 86 | 87 | 88 | def here(): 89 | return dirname(abspath(unifilename(__file__))) 90 | 91 | 92 | def rel(filename): 93 | fn = os.path.join(here(), unistr(filename)) 94 | try: 95 | if os.path.exists(fn): 96 | return fn 97 | except UnicodeEncodeError: 98 | pass 99 | return findfile(fn) 100 | 101 | 102 | def uniglob(path): 103 | """This function transforms a path into a glob pattern 104 | in which every non-ascii character of the path is replaced by a '*'. 105 | Finally consecutive '*' characters are compressed into a single one. 106 | """ 107 | filepath = os.path.normpath(path) 108 | pattern = re_unisubst.sub('*', filepath) 109 | pattern = re_uniglob.sub('*', pattern) 110 | return pattern 111 | 112 | 113 | def findfile(filename): 114 | matches = glob.glob(uniglob(sysfilename(filename))) 115 | assert len(matches) == 1 116 | return matches[0] 117 | 118 | 119 | def intl_filename(): 120 | return rel(INTL_PROJECT_FILENAME) 121 | 122 | 123 | def template(objectversion=46, objects='', top=''): 124 | return PROJECT_TEMPLATE % {'objectVersion': objectversion, 125 | 'objects': objects, 126 | 'toplevel': top} 127 | 128 | 129 | def parse(prj, report=True, fp=None, **kwargs): 130 | root, parseinfo = xcodeprojer.parse(prj, **kwargs) 131 | if report: 132 | xcodeprojer.report_parse_status(root, parseinfo, filename=None, fp=fp) 133 | return root, parseinfo 134 | 135 | 136 | def sparse(prj, **kwargs): 137 | """Parse without additional error reporting. 138 | """ 139 | return parse(prj, report=False, **kwargs) 140 | 141 | 142 | def unparse(root, **kwargs): 143 | return xcodeprojer.unparse(root, **kwargs).decode('utf-8') 144 | 145 | 146 | def read_file(filename): 147 | with open(filename, 'rb') as f: 148 | return f.read() 149 | 150 | 151 | def read_project(relpath): 152 | filename = rel(relpath) 153 | prj = read_file(filename) 154 | prj = unistr(prj) 155 | return prj, filename 156 | 157 | 158 | def read_intl_project(): 159 | return read_project(INTL_PROJECT_FILENAME) 160 | 161 | 162 | def read_mini_project(): 163 | return read_project(MINI_PROJECT_FILENAME) 164 | 165 | 166 | def find_isa(node, isa): 167 | for v in node.values(): 168 | if v.get('isa') == isa: 169 | return v 170 | return None 171 | 172 | 173 | def run_args(*args): 174 | outbuf, errbuf = StringIO(), StringIO() 175 | with RedirectStdStreams(stdout=outbuf, stderr=errbuf): 176 | parser = xcodeprojer.cmdline_parser(ErrorRecordingArgumentParser) 177 | args = parser.parse_args(*args) 178 | ret = xcodeprojer.run_with_args(args, parser) 179 | return ret, outbuf.getvalue(), errbuf.getvalue() 180 | 181 | 182 | class ParserTestCase(unittest.TestCase): 183 | 184 | XML_TEMPLATE = """ 185 | 186 | 187 | 188 | objectVersion 189 | 46 190 | objects 191 | 192 | list 193 | 194 | 1 195 | >'.'< 196 | 197 | 198 | """ 199 | 200 | def test_array(self): 201 | prj = template(top="""an_array = (1, 2, 3,);""") 202 | root, parseinfo = sparse(prj) 203 | self.assertEqual(root['an_array'], ['1', '2', '3']) 204 | 205 | def test_array_without_terminator(self): 206 | prj = template(top="""an_array = (1, 2, 3);""") 207 | root, parseinfo = sparse(prj) 208 | self.assertEqual(root['an_array'], ['1', '2', '3']) 209 | 210 | def test_array_without_separators(self): 211 | prj = template(top="""an_array = (1 2 3);""") 212 | root, parseinfo = sparse(prj) 213 | self.assertIsNone(root) 214 | 215 | def test_dictionary(self): 216 | prj = template(top="""a_dictionary = { KEY = VALUE; };""") 217 | root, parseinfo = sparse(prj) 218 | self.assertEqual(root['a_dictionary'], {'KEY': 'VALUE'}) 219 | 220 | def test_dictionary_without_terminator(self): 221 | prj = template(top="""a_dictionary = { KEY = VALUE };""") 222 | root, parseinfo = sparse(prj) 223 | self.assertIsNone(root) 224 | 225 | def test_unparse(self): 226 | prj = template(top="""an_array = (1 , 2 , 3);""") 227 | root, parseinfo = sparse(prj) 228 | assert root 229 | 230 | out = unparse(root) 231 | a = """ 232 | an_array = ( 233 | 1, 234 | 2, 235 | 3, 236 | );""" 237 | self.assertEqual(out, template(top=a)) 238 | 239 | def test_xml_parse(self): 240 | xml = self.XML_TEMPLATE 241 | root, parseinfo = parse(xml) 242 | self.assertEqual(root, {'objects': {}, 'list': ['1', '>\'.\'<'], 'objectVersion': '46'}) 243 | 244 | def test_xml_parse_error(self): 245 | xml = self.XML_TEMPLATE 246 | xml = xml.replace('', '') 247 | buf = StringIO() 248 | root, parseinfo = parse(xml, report=True, fp=buf) 249 | self.assertIsNone(root) 250 | self.assertEqual(buf.getvalue(), 'File , line 3, column 15\n' 251 | '\n' 252 | ' ^\n' 253 | 'Error: parsing XML failed\n') 254 | 255 | def test_recursionlimit(self): 256 | prj, filename = read_mini_project() 257 | one_comment = '/* Begin PBXBuildFile section */\n' 258 | for opener in '(', '{': 259 | many_dicts = one_comment + 200 * opener 260 | modprj = prj.replace(one_comment, many_dicts) 261 | root, parseinfo = parse(modprj, report=False, format='xcode', parsertype='classic') 262 | self.assertIsNone(root) 263 | 264 | 265 | class ParserErrorTestCase(unittest.TestCase): 266 | 267 | def test_space_in_unquoted(self): 268 | prj, filename = read_mini_project() 269 | pos = prj.find('DeploymentPostprocessing') 270 | prj = prj[:pos] + ' ' + prj[pos:] 271 | 272 | for format, parsertype in [(None, 'normal'), ('xcode', 'fast'), ('xcode', 'classic')]: 273 | buf = StringIO() 274 | root, parseinfo = parse(prj, format=format, parsertype=parsertype, 275 | report=True, fp=buf) 276 | self.assertIsNone(root) 277 | report = unistr(buf.getvalue()) 278 | 279 | if parsertype == 'fast': 280 | self.assertTrue(report.find('Error: parsing Xcode plist via JSON failed') >= 0) 281 | else: 282 | self.assertEqual(report, 'File , line 21, column 24\n' 283 | '\t\t\trunOnlyFor DeploymentPostprocessing = 1;\n' 284 | '\t\t\t ^~~~~~~~~~~~~~~~~~~~~~~~\n' 285 | 'Error: parsing Xcode plist classically failed\n') 286 | 287 | def test_outoftokens(self): 288 | prj, filename = read_mini_project() 289 | closingbracepos = prj.rfind('}') 290 | # Without a closing brace the parsers should run out of tokens 291 | # and report the parse error. 292 | prj = prj[:closingbracepos] 293 | expected_line_columns = { 294 | (None, 'normal'): (168, 62), 295 | ('xcode', 'fast'): (1, 2994), 296 | ('xcode', 'classic'): (168, 62), 297 | } 298 | for format, parsertype in [(None, 'normal'), ('xcode', 'fast'), ('xcode', 'classic')]: 299 | buf = StringIO() 300 | root, parseinfo = parse(prj, format=format, parsertype=parsertype, 301 | report=True, fp=buf) 302 | self.assertIsNone(root) 303 | report = buf.getvalue() 304 | expline, expcolumn = expected_line_columns[(format, parsertype)] 305 | numbers = [int(x) for x in re.split(r'\D+', report) if x] 306 | line, column = numbers[:2] 307 | self.assertEqual(line, expline) 308 | # The JSON parser column report differs between Python versions. 309 | self.assertTrue(column == expcolumn or column == expcolumn + 1) 310 | 311 | 312 | class IntlTestCase(unittest.TestCase): 313 | 314 | def test_i18n(self): 315 | prj, filename = read_intl_project() 316 | prj = bytestr(prj) 317 | prjname = xcodeprojer.projectname_for_path(filename) 318 | 319 | for parsertype in ['fast', 'classic']: 320 | root, parseinfo = parse(prj, format='xcode', parsertype=parsertype) 321 | self.assertIsNotNone(root, "parsing with parsertype %s failed" % parsertype) 322 | pbxproject = find_isa(root['objects'], 'PBXProject') 323 | orgname = pbxproject['attributes']['ORGANIZATIONNAME'] 324 | self.assertEqual(orgname, u('🎫'), 325 | "unexpected ORGANIZATIONNAME '%s' after parsing with parsertype %s." 326 | % (pbxproject['attributes']['ORGANIZATIONNAME'], parsertype)) 327 | 328 | output = xcodeprojer.unparse(root, format='xcode', projectname=prjname) 329 | if prj != output: 330 | xcodeprojer.print_diff(prj, output, filename=filename) 331 | 332 | self.assertEqual(prj, output) 333 | 334 | def check_format_tree(self, format): 335 | prj, filename = read_intl_project() 336 | prj = bytestr(prj) 337 | prjname = xcodeprojer.projectname_for_path(filename) 338 | plistroot, parseinfo = parse(prj, format='xcode') 339 | self.assertIsNotNone(plistroot) 340 | 341 | fmtfilename = os.path.join(dirname(filename), 'project.%s' % format) 342 | formattext = read_file(fmtfilename) 343 | root, parseinfo = parse(formattext, format=format) 344 | self.assertIsNotNone(root) 345 | self.assertEqual(root, plistroot) 346 | 347 | root, parseinfo = parse(prj) 348 | output = xcodeprojer.unparse(root, format=format, projectname=prjname) 349 | if formattext != output: 350 | xcodeprojer.print_diff(formattext, output, filename=filename) 351 | self.assertEqual(formattext, output) 352 | 353 | def test_xml(self): 354 | self.check_format_tree('xml') 355 | 356 | def test_json(self): 357 | self.check_format_tree('json') 358 | 359 | def test_unicode_in_unquoted(self): 360 | prj, filename = read_mini_project() 361 | prj = prj.replace(u('ORGANIZATIONNAME = "ACME Inc.";'), 362 | u('ORGANIZATIONNAME = 🍏;')) 363 | prj = bytestr(prj) 364 | for format, parsertype in [(None, 'normal'), ('xcode', 'fast'), ('xcode', 'classic')]: 365 | buf = StringIO() 366 | root, parseinfo = parse(prj, format=format, parsertype=parsertype, 367 | report=True, fp=buf) 368 | self.assertIsNone(root) 369 | 370 | 371 | class PlutilTestCase(unittest.TestCase): 372 | 373 | # Verifying that our XML generator matches plutil(1) 374 | # would only check if plutil has changed and slow down the unit tests. 375 | def test_plutil(self): 376 | if TEST_AGAINST_PLUTIL: 377 | filename = intl_filename() 378 | fmtfilename = os.path.join(dirname(filename), 'project.json') 379 | jsondata = read_file(fmtfilename) 380 | 381 | fmtfilename = os.path.join(dirname(filename), 'project.xml') 382 | ourxml = read_file(fmtfilename) 383 | 384 | plutil_xml = plutil(jsondata, '-convert', 'xml1', '-o', '-') 385 | self.assertEqual(ourxml, plutil_xml) 386 | 387 | # --------------------------------------------------------------- 388 | # This is RedirectStdStreams from an answer by Rob Cowie on 389 | # http://stackoverflow.com/questions/6796492/python-temporarily-redirect-stdout-stderr/6796752#6796752 390 | # which is used to record the output of xcoderprojer as if called from the command line. 391 | 392 | 393 | class RedirectStdStreams(object): 394 | def __init__(self, stdout=None, stderr=None): 395 | self._stdout = stdout or sys.stdout 396 | self._stderr = stderr or sys.stderr 397 | 398 | def __enter__(self): 399 | self.old_stdout, self.old_stderr = sys.stdout, sys.stderr 400 | self.old_stdout.flush(); self.old_stderr.flush() 401 | sys.stdout, sys.stderr = self._stdout, self._stderr 402 | 403 | def __exit__(self, exc_type, exc_value, traceback): 404 | self._stdout.flush(); self._stderr.flush() 405 | sys.stdout = self.old_stdout 406 | sys.stderr = self.old_stderr 407 | 408 | # --------------------------------------------------------------- 409 | 410 | 411 | class ErrorRecordingArgumentParser(argparse.ArgumentParser): 412 | 413 | def __init__(self, *args, **kwargs): 414 | super(ErrorRecordingArgumentParser, self).__init__(*args, **kwargs) 415 | self.testerrors = [] 416 | 417 | def error(self, message): 418 | self.testerrors.append(message) 419 | 420 | 421 | class CmdlineTestCase(unittest.TestCase): 422 | 423 | def test_convert(self): 424 | prj, filename = read_intl_project() 425 | ret, outtxt, errtxt = run_args(['-o', '-', '--convert', 'xcode', filename]) 426 | self.assertEqual(ret, xcodeprojer.OK) 427 | self.assertEqual(outtxt, prj) 428 | 429 | def test_convert_multiple_filenames(self): 430 | prj, filename = read_intl_project() 431 | ret, outtxt, errtxt = run_args(['-o', '-', '--convert', 'xcode', filename, filename]) 432 | self.assertEqual(ret, xcodeprojer.ERROR) 433 | self.assertEqual(outtxt, '') 434 | 435 | def test_lint(self): 436 | filename = rel(INTL_PROJECT_FILENAME) 437 | ret, outtxt, errtxt = run_args(['--lint', '-o', '-', filename]) 438 | self.assertEqual(ret, xcodeprojer.OK) 439 | self.assertEqual(outtxt, '') 440 | 441 | filename = splitext(filename)[0] + '.json' 442 | ret, outtxt, errtxt = run_args(['--lint', '-o', '-', filename]) 443 | self.assertEqual(ret, xcodeprojer.LINT_FAILED) 444 | expectedend = 'is in json which is nothing that Xcode can read.\n' 445 | self.assertEqual(outtxt[-len(expectedend):], expectedend) 446 | 447 | filename = splitext(filename)[0] + '.xml' 448 | ret, outtxt, errtxt = run_args(['--lint', '-o', '-', filename]) 449 | self.assertEqual(ret, xcodeprojer.LINT_FAILED) 450 | expectedend = 'is in XML which is a clearly a failed lint.\n' 451 | self.assertEqual(outtxt[-len(expectedend):], expectedend) 452 | 453 | def test_gidsplit(self): 454 | ret, outtxt, errtxt = run_args(['--gidsplit', '4CDE96A219B3613C009DF310']) 455 | self.assertEqual(ret, 0) 456 | 457 | def test_giddump(self): 458 | filename = rel(INTL_PROJECT_FILENAME) 459 | self.assertTrue(os.path.exists(filename)) 460 | ret, outtxt, errtxt = run_args(['--gid-format', 'json', '--giddump', filename]) 461 | self.assertEqual(ret, 0) 462 | 463 | # Only test a part of the dump. 464 | root = json.loads(outtxt) 465 | self.assertEqual(len(root['gids']), 18) 466 | gid = '4C36A8C719A0D91D00F6C76D' 467 | for obj in root['gids']: 468 | if obj['gid'] == gid: 469 | jobj = json.dumps(obj, sort_keys=True, indent=20, separators=(',', ':')) 470 | self.assertEqual(jobj, r"""{ 471 | "comment":"Build configuration list for PBXProject \"I\u014b\u01ab\u0190rn\u00e6ti\u00f8n\u00e6l X\u00a9\u00f8d\u01dd \u00b6\u00ae\u00f8j\u00e6\u0188t\"", 472 | "date":"2014-08-17T12:35:41Z", 473 | "gid":"4C36A8C719A0D91D00F6C76D", 474 | "pid":54, 475 | "random":16172909, 476 | "seq":43207, 477 | "user":76 478 | }""") 479 | break 480 | else: 481 | self.assertFalse("The object %r cound not be found." % gid) 482 | 483 | # --------------------------------------------------------------------- 484 | # 485 | # plutil(1) can be used to verify correct translation for XML and JSON. 486 | # 487 | 488 | def plutil(inputdata, *args): 489 | """ 490 | e.g.: 491 | plutil(inputdata, '-convert', 'xml1', '-o', '-') 492 | """ 493 | try: 494 | plargs = ['/usr/bin/plutil'] + list(args) + ['-'] 495 | client = subprocess.Popen(plargs, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 496 | output = client.communicate(input=inputdata)[0] 497 | return output 498 | except OSError: 499 | return 500 | 501 | 502 | # --------------------------------------------------------------- 503 | 504 | class GlobalIDTestCase(unittest.TestCase): 505 | 506 | def setUp(self): 507 | pass 508 | 509 | def tearDown(self): 510 | pass 511 | 512 | def timefunc(self): 513 | t = self.seconds 514 | self.seconds += self.seconds_increment 515 | return t 516 | 517 | def test_globalid(self): 518 | # When the generator is initialized this timestamp feeds into the random generator. 519 | # On the first call of generate() the timefunc is called for the second time, 520 | # that is why the first gid has a timestamp 10 seconds later than the time we specified. 521 | utctimestamp = "2014-07-29 17:04:20Z" 522 | testdate = datetime.datetime.strptime(utctimestamp, "%Y-%m-%d %H:%M:%SZ") 523 | secs = xcodeprojer.UniqueXcodeIDGenerator.reftime(cal.timegm(testdate.utctimetuple())) 524 | self.seconds = secs 525 | self.seconds_increment = 10 526 | 527 | gen = xcodeprojer.UniqueXcodeIDGenerator(username='unrecompiled', pid=56007, refdatefunc=self.timefunc) 528 | if PY2: 529 | self.assertEqual(gen.generate(), '4CC7BE4419880B9E009C9D7C') 530 | self.assertEqual(gen.generate(), '4CC7BE4519880BA8009C9D7C') 531 | self.assertEqual(gen.generate(), '4CC7BE4619880BB2009C9D7C') 532 | self.assertEqual(gen.generate(), '4CC7BE4719880BBC009C9D7C') 533 | else: 534 | # Python 3 has a different random number generator 535 | self.assertEqual(gen.generate(), '4CC742AD19880B9E00393AF0') 536 | self.assertEqual(gen.generate(), '4CC742AE19880BA800393AF0') 537 | self.assertEqual(gen.generate(), '4CC742AF19880BB200393AF0') 538 | self.assertEqual(gen.generate(), '4CC742B019880BBC00393AF0') 539 | 540 | def test_gidsplit(self): 541 | buf = StringIO() 542 | xcodeprojer.gidsplit(['4CC7BE4419880B9E009C9D7C', '4CC7BE4719880BBC009C9D7C'], buf=buf) 543 | text = buf.getvalue() 544 | expected = '2014-07-29T17:04:30Z 76 199 10263932 48708 4CC7BE4419880B9E009C9D7C\n' \ 545 | '2014-07-29T17:05:00Z 76 199 10263932 48711 4CC7BE4719880BBC009C9D7C\n' 546 | self.assertEqual(text, expected) 547 | 548 | buf = StringIO() 549 | xcodeprojer.gidsplit(['4CC7BE4419880B9E009C9D7C', '4CC7BE4719880BBC009C9D7C'], format='json', buf=buf) 550 | text = buf.getvalue() 551 | expected = """{ 552 | "gids":[ 553 | { 554 | "date":"2014-07-29T17:04:30Z", 555 | "gid":"4CC7BE4419880B9E009C9D7C", 556 | "pid":199, 557 | "random":10263932, 558 | "seq":48708, 559 | "user":76 560 | }, 561 | { 562 | "date":"2014-07-29T17:05:00Z", 563 | "gid":"4CC7BE4719880BBC009C9D7C", 564 | "pid":199, 565 | "random":10263932, 566 | "seq":48711, 567 | "user":76 568 | } 569 | ] 570 | } 571 | """ 572 | self.assertEqual(text, expected) 573 | 574 | # --------------------------------------------------------------- 575 | 576 | if __name__ == '__main__': 577 | unittest.main() 578 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py32, py33, py34, pypy 3 | skip_missing_interpreters=True 4 | 5 | [testenv] 6 | setenv = LC_ALL=C 7 | commands = 8 | python tests/test_xcodeprojer.py 9 | python tests/check_xcode_behaviour.py --diverse 10 | python examples/examine_local_projects.py --find . 11 | python examples/examine_local_projects.py --test 12 | python examples/gidhistograms.py --emoji . 13 | -------------------------------------------------------------------------------- /xcodeprojer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2014 Michael Krause ( http://krause-software.com/ ). 5 | # 6 | # You are free to use this code under the MIT license: 7 | # http://opensource.org/licenses/MIT 8 | 9 | # xcoderprojer.py is a script for converting project.pbxproj files 10 | # that represent Xcode projects into one of three formats, 11 | # commented Plist, XML and JSON. 12 | # 13 | # It can also be used to simply check if a project file is in the 14 | # correct form via the lint option. 15 | # 16 | # Xcode uses global ids (strings of 24 hex digits) as references 17 | # in project files. These ids are only partially random 18 | # and they have a structure that can be constructed 19 | # or taken apart with this script. 20 | 21 | from __future__ import print_function 22 | 23 | __version__ = '0.1' 24 | 25 | import sys 26 | 27 | if (((sys.version_info.major == 2) and (sys.version_info.minor < 7)) 28 | or ((sys.version_info.major == 3) and (sys.version_info.minor < 2))): 29 | sys.stderr.write("Error: Python 2.7 or when running on Python 3 at least Python 3.2 is required to run xcodeprojer\n") 30 | sys.exit(1) 31 | 32 | import os 33 | import re 34 | import argparse 35 | import unicodedata 36 | import time 37 | import random 38 | import json 39 | import datetime 40 | import difflib 41 | import tempfile 42 | import codecs 43 | from operator import xor 44 | from io import BytesIO 45 | 46 | from collections import OrderedDict 47 | 48 | try: 49 | import xml.etree.cElementTree as ETree 50 | except ImportError: 51 | import xml.etree.ElementTree as ETree 52 | 53 | 54 | __all__ = ['parse', 'unparse', 'report_parse_status', 'projectname_for_path', 55 | 'print_diff', 'is_global_id', 'find_projectfiles', 56 | 'UniqueXcodeIDGenerator', 'gidfields'] 57 | 58 | PBXPROJNAME = 'project.pbxproj' 59 | 60 | OK = 0 61 | ERROR = 1 62 | PARSING_FAILED = 1 63 | 64 | LINT_DIFFERENCES = 1 65 | LINT_FAILED = 2 66 | 67 | CONVERT_OUTPUT_FAILED = 2 68 | 69 | 70 | LATEST_OBJECT_VERSION = 46 71 | 72 | STDIN = None 73 | STDOUT = '-' 74 | 75 | PY2 = sys.version_info[0] == 2 76 | PY3 = sys.version_info[0] == 3 77 | 78 | args_info = set([]) 79 | args_debug = set([]) 80 | args_verbose = set([]) 81 | 82 | 83 | if PY2: 84 | text_type = unicode 85 | binary_type = str 86 | else: 87 | text_type = str 88 | binary_type = bytes 89 | unichr = chr 90 | 91 | 92 | def unistr(text): 93 | if not isinstance(text, text_type): 94 | text = text.decode('utf-8') 95 | return text 96 | 97 | 98 | def bytestr(text): 99 | if not isinstance(text, binary_type): 100 | text = text.encode('utf-8') 101 | return text 102 | 103 | 104 | INFO_TIME = 'time' 105 | INFO_BACKTRACKS = 'backtracks' 106 | INFO_ALL = [INFO_TIME, INFO_BACKTRACKS] 107 | 108 | verbose_categories = { 109 | 1: [INFO_TIME], 110 | 2: [INFO_BACKTRACKS], 111 | } 112 | 113 | DEBUG_OPTIONS = 'options' 114 | DEBUG_ALL = [DEBUG_OPTIONS] 115 | 116 | 117 | re_unescape = re.compile(r'\\\\|\\"|\\a|\\b|\\t|\\v|\\f|\\n|(?:\\U[0-9a-f]{4})') 118 | unescape_dict = { 119 | '\\\\': '\\', 120 | '\\"': '"', 121 | '\\a': '\a', 122 | '\\b': '\b', 123 | '\\t': '\t', 124 | '\\v': '\v', 125 | '\\f': '\f', 126 | '\\n': '\n', 127 | } 128 | 129 | re_escape = re.compile(r'[\x00-\x1f"\\]') 130 | escape_dict = {v: k for k, v in unescape_dict.items()} 131 | 132 | 133 | # In comparison with unquotedstring in r_tokenize Xcode quotes 134 | # some characters even when they are accepted in unquoted strings. 135 | r_quoteworthy = re.compile(r'[^a-zA-Z0-9$./_]|___') 136 | r_gid = re.compile(r'\A[0-9A-Z]{24}\Z') 137 | r_ws = re.compile(r'\s*') 138 | 139 | # Here we have the tokenizing expression that splits any Xcode plist 140 | # into its tokens. We do not create tokens for whitespace because we 141 | # don't need them and it would significantly slow the parsers down. 142 | r_tokenize = re.compile(r""" 143 | (?: 144 | (?P//.*$) # linecomment 145 | | (?P/\*.*?\*/) # comment 146 | # unquoted values can contain slashes so we must handle the two comment forms 147 | # before unquotedstring. 148 | | (?P[$./:_a-zA-Z0-9-]+) # unquotedstring which might be an Xcode global id (24 hex digits) 149 | | (?P=) # equals 150 | | (?P;) # key-value pair terminator 151 | | (?P"(?:\\"|[^"])*") # a doule-quoted string 152 | | (?P\{) # start of a dictionary 153 | | (?P\}) # end of a dictionary 154 | | (?P,) # array element terminator 155 | | (?P\() # start of an array 156 | | (?P\)) # end of an array 157 | ) 158 | \s* # skip whitespace after each token 159 | """, re.MULTILINE | re.VERBOSE) 160 | 161 | 162 | rule_mapping = r_tokenize.groupindex 163 | rulenumber_to_name = dict((vnr, kname) for (kname, vnr) in rule_mapping.items()) 164 | rule_names = [rulenumber_to_name.get(groupnr, 'dummy') for groupnr in range(r_tokenize.groups + 1)] 165 | 166 | RULE_COMMENT = rule_mapping['comment'] 167 | RULE_LINECOMMENT = rule_mapping['linecomment'] 168 | RULE_UNQUOTEDSTRING = rule_mapping['unquotedstring'] 169 | RULE_EQUALS = rule_mapping['equals'] 170 | RULE_SEMICOLON = rule_mapping['semicolon'] 171 | RULE_QUOTEDSTRING = rule_mapping['quotedstring'] 172 | RULE_DICTIONARY = rule_mapping['dictionary'] 173 | RULE_DICTIONARYEND = rule_mapping['dictionaryend'] 174 | RULE_COMMA = rule_mapping['comma'] 175 | RULE_ARRAY = rule_mapping['array'] 176 | RULE_ARRAYEND = rule_mapping['arrayend'] 177 | 178 | 179 | def parse(text, format=None, dictionarytype=dict, parsertype='normal'): 180 | """Parses the Xcode project as binary text 181 | and creates the tree of nested dicts, arrays and strings 182 | that represents the original structure. 183 | 184 | :param text: the content of a project.pbxproj. 185 | :param format: one of 'xcode', 'xml', 'json' or None for automatic detection. 186 | :param dictionarytype: should be dict or OrderedDict. 187 | :param parsertype: normal: parse plists via syntax transformation by the fast JSON parser 188 | and use the classic parser if the JSON parser failed. 189 | fast: only parse with the fast JSON parser. 190 | classic: only parse plists with the classic parser. 191 | :return: the tuple (rootnode, parseinfo). 192 | """ 193 | text = unistr(text) 194 | 195 | root, parseinfo = None, None 196 | can_only_be_xml = text.startswith('= 0 and qend >= 0 and qstart + 1 < qend: 339 | return text[qstart+1:qend] 340 | return None 341 | 342 | 343 | def projectname_for_path(filename): 344 | name, ext = os.path.splitext(os.path.basename(os.path.dirname(filename))) 345 | if ext not in ['.xcodeproj', '.xcode', '.pbproj', '.pbxproj']: 346 | return None 347 | return decode_utf8_or_sys(name) 348 | 349 | 350 | def is_global_id(node): 351 | return r_gid.match(node) is not None 352 | 353 | 354 | def escape_str(text): 355 | def replace(m): 356 | char = m.group(0) 357 | return escape_dict.get(char) or ('\\U%04x' % ord(char)) 358 | 359 | return re_escape.sub(replace, text) 360 | 361 | 362 | def unescape_str(s): 363 | def replace(m): 364 | txt = m.group(0) 365 | c = unescape_dict.get(txt) 366 | if c is not None: 367 | return c 368 | if s.startswith('\\U'): 369 | return unichr(int(txt[2:], 16)) 370 | else: 371 | raise ValueError('unknown escape sequence: %s' % repr(s)) 372 | 373 | return re_unescape.sub(replace, s[1:-1]) 374 | 375 | 376 | def quoted_string(s): 377 | # Xcode always quotes strings containing a triple underscore 378 | # because these strings are destined for textual replacement 379 | # with strings that may contain spaces and other quoteworthy characters. 380 | if not s: 381 | return '""' 382 | if r_quoteworthy.search(s) is not None: 383 | return '"' + escape_str(s) + '"' 384 | return s 385 | 386 | 387 | def parse_xcodeproject_json(text, dictionarytype=dict): 388 | try: 389 | text = unistr(text) 390 | root = json.loads(text, object_pairs_hook=dictionarytype) 391 | parseinfo = {'format': 'json'} 392 | return root, parseinfo 393 | except ValueError as e: 394 | linenr, column, errortext = error_report_from('JSON', text, text_type(e)) 395 | parseinfo = {'error_column': column, 396 | 'error_line_number': linenr, 397 | 'error_text': errortext} 398 | return None, parseinfo 399 | 400 | 401 | def parse_xcodeproject_plist_direct(text, dictionarytype=dict): 402 | t0 = time.time() 403 | text = unistr(text) 404 | 405 | tokenrules = [] 406 | tokentexts = [] 407 | offsets = [] 408 | num_comments = 0 409 | 410 | prjname = None 411 | pos = skip_whitespace(text) 412 | for m in r_tokenize.finditer(text, pos): 413 | if m.start() != pos: 414 | # The found fragments must be contiguous for a valid parse 415 | break 416 | rule_number = m.lastindex 417 | # Only count comments to eventually unparse the file 418 | # with or without comments depending on if there were comments. 419 | if rule_number == RULE_COMMENT: 420 | # The unparser needs the project name which should be known from the filename 421 | # or from a parameter that was passed in. Pulling the project name 422 | # out of a comment is our only other chance but still it might be wrong. 423 | if prjname is None: 424 | prjname = projectname_from_comment(m.group(rule_number)) 425 | num_comments += 1 426 | elif rule_number == RULE_LINECOMMENT: 427 | pass 428 | else: 429 | tokenrules.append(rule_number) 430 | tokentexts.append(m.group(rule_number)) 431 | offsets.append(pos) 432 | 433 | pos = m.end() 434 | 435 | lastpos = pos 436 | 437 | parseinfo = {'num_comments': num_comments} 438 | success, root = parse_tokens(tokenrules, tokentexts, 439 | dictionarytype=dictionarytype) 440 | 441 | if not success: 442 | tokenpos = errortokenpos(root) 443 | if tokenpos < len(tokenrules): 444 | errstart = offsets[tokenpos] 445 | errstop = errstart + len(tokentexts[tokenpos]) 446 | else: 447 | errstart = errstop = lastpos 448 | parseinfo.update(error_report_dict(text, errstart, errstop, 'Xcode plist classically')) 449 | return None, parseinfo 450 | 451 | parseinfo = { 452 | 'format': 'xcode', 453 | 'parsetime': time.time() - t0, 454 | 'parser': 'classic', 455 | } 456 | if prjname is not None: 457 | parseinfo['projectname'] = prjname 458 | return root, parseinfo 459 | 460 | 461 | def errortokenpos(tokenoffset): 462 | """Find the root cause in the chain of parser errors 463 | and return the token position where the parser could 464 | no longer advance. 465 | """ 466 | if isinstance(tokenoffset, ParserError): 467 | for prevexc in iter(lambda: tokenoffset.prevexc, None): 468 | tokenoffset = prevexc 469 | tokenoffset = tokenoffset.pos 470 | return tokenoffset 471 | 472 | 473 | def error_report_dict(text, errstart, errstop, formatdesc): 474 | linenr, column, errortext = parse_error_report(text, errstart, errstop, formatdesc) 475 | return {'error_column': column, 476 | 'error_line_number': linenr, 477 | 'error_text': errortext} 478 | 479 | 480 | def linenr_column_line(text, offset): 481 | """Return line number, column and the whole line 482 | in which text[offset] lies. 483 | Line number and column are in one-based indexing. 484 | Each tab is counted as one column. 485 | """ 486 | 487 | offset = min(max(0, offset), len(text)) 488 | textbegin = text[:offset] 489 | if not textbegin: 490 | return 1, 1, None 491 | lines = textbegin.splitlines(True) 492 | linenr, column = max(1, len(lines)), len(lines[-1]) + 1 493 | line = lines[-1] 494 | nlpos = text.find('\n', offset) 495 | if nlpos >= 0: 496 | line += text[offset:nlpos+1] 497 | else: 498 | line += text[offset:] 499 | return linenr, column, line 500 | 501 | 502 | def parse_error_report(text, errstart, errstop, formatdesc): 503 | errmsg = 'Error: parsing %s failed' % formatdesc 504 | 505 | linenr, column, line = linenr_column_line(text, errstart) 506 | if line is None: 507 | return 1, 1, errmsg 508 | 509 | if errstop >= len(text): 510 | return linenr, column, errmsg + ', reached end of text before completing the grammar' 511 | 512 | # Use a line in which all printables are replaced by spaces 513 | # as a prefix to position the caret under the error position. 514 | blankline = re.sub(r'\S', ' ', line) 515 | blankline = blankline[:column-1] 516 | 517 | # We count each tab as four spaces and report the 518 | # column number after this expansion as this is common 519 | # in the column display of text editors. 520 | countingline = blankline.replace('\t', ' ') 521 | column = len(countingline) + 1 522 | 523 | # Draw squiggles under the offending token that 524 | # follows after the error position. 525 | errtoken = text[errstart:errstop] 526 | errtoken = errtoken.replace('\t', ' ') 527 | caretline = blankline + '^' + '~' * (len(errtoken) - 1) 528 | errortext = line.rstrip() + '\n' + caretline + '\n' 529 | errortext += errmsg 530 | 531 | return linenr, column, errortext 532 | 533 | 534 | def error_report_from(format, text, errortext): 535 | """XML and JSON parse error texts look like this: 536 | not well-formed (invalid token): line 155, column 8 537 | or Expecting ':' delimiter: line 14 column 32 (char 303) 538 | 539 | Extract the line and column information to report the offending 540 | line with a caret positioned under the shady character. 541 | """ 542 | linepos = errortext.rfind('line') 543 | errmsg = 'Error: parsing %s failed' % format 544 | standard_error = 1, 1, errmsg 545 | if linepos == -1: 546 | return standard_error 547 | errortext = errortext[linepos:] 548 | numbers = [int(x) for x in re.split(r"[^\d]+", errortext) if x] 549 | if len(numbers) not in [2, 3]: # Do we have line, column, [char]? 550 | return standard_error 551 | linenr, column = numbers[:2] 552 | 553 | errstart = 0 554 | for idx, line in enumerate(text.splitlines(True)): 555 | if idx + 1 == linenr: 556 | errstart += column - 1 557 | break 558 | errstart += len(line) 559 | else: 560 | # If the line containing the error wasn't found 561 | # no extended error report is genenerated 562 | return linenr, column, errmsg 563 | 564 | return parse_error_report(text, errstart, errstart, format) 565 | 566 | 567 | class ParserError(Exception): 568 | 569 | def __init__(self, text, pos, prevexc=None): 570 | super(ParserError, self).__init__(text) 571 | self.pos = pos 572 | self.prevexc = prevexc 573 | 574 | 575 | def parse_tokens(tokenrules, tokentexts, dictionarytype=dict): 576 | 577 | def inc(pos): 578 | return pos + 1 579 | 580 | def rule_at(pos): 581 | try: 582 | return tokenrules[pos] 583 | except IndexError: 584 | raise ParserError('Expecting more tokens', pos) 585 | 586 | def text_at(pos): 587 | try: 588 | return tokentexts[pos] 589 | except IndexError: 590 | raise ParserError('Expecting more tokens', pos) 591 | 592 | def skip_literal(pos, rulenr): 593 | if rule_at(pos) == rulenr: 594 | return inc(pos) 595 | return pos 596 | 597 | def musthave_literal(pos, s): 598 | nextpos = skip_literal(pos, s) 599 | if nextpos == pos: 600 | raise ParserError("Expecting '%s'" % s, pos) 601 | return nextpos 602 | 603 | def sequence(pos, *args): 604 | elements = [] 605 | for element in args: 606 | if callable(element): 607 | r = element(pos) 608 | v, pos = r 609 | elements.append(v) 610 | else: 611 | pos = musthave_literal(pos, element) 612 | return elements, pos 613 | 614 | def zero_or_more(pos, func, nextelem): 615 | elements = [] 616 | while True: 617 | try: 618 | v, pos = func(pos) 619 | elements.append(v) 620 | except ParserError as e: 621 | try: 622 | musthave_literal(pos, nextelem) 623 | except ParserError as e2: 624 | e2.prevexc = e 625 | raise e2 626 | return elements, pos 627 | return elements, pos 628 | 629 | def anystring(pos): 630 | rulenr = rule_at(pos) 631 | if rulenr == RULE_UNQUOTEDSTRING: 632 | return unquotedstring(pos) 633 | elif rulenr == RULE_QUOTEDSTRING: 634 | return quotedstring(pos) 635 | raise ParserError('Expecting string', pos) 636 | 637 | def value(pos): 638 | rulenr = rule_at(pos) 639 | if rulenr == RULE_UNQUOTEDSTRING: 640 | return unquotedstring(pos) 641 | elif rulenr == RULE_DICTIONARY: 642 | return dictionary(pos) 643 | elif rulenr == RULE_QUOTEDSTRING: 644 | return quotedstring(pos) 645 | elif rulenr == RULE_ARRAY: 646 | return array(pos) 647 | raise ParserError('Expecting value', pos) 648 | 649 | def arrayvalue(pos): 650 | rulenr = rule_at(pos) 651 | if rulenr == RULE_UNQUOTEDSTRING: 652 | return unquotedstring(pos) 653 | elif rulenr == RULE_QUOTEDSTRING: 654 | return quotedstring(pos) 655 | elif rulenr == RULE_DICTIONARY: 656 | return dictionary(pos) 657 | raise ParserError('Expecting array value', pos) 658 | 659 | def arrayelements(pos): 660 | try: 661 | return zero_or_more(pos, arrayelement, RULE_ARRAYEND) 662 | except RuntimeError as exc: 663 | if str(exc).startswith('maximum recursion depth exceeded'): 664 | raise ParserError("Exceeded maximum recursion", pos) 665 | 666 | def arrayelement(pos): 667 | v, pos = arrayvalue(pos) 668 | 669 | p2 = skip_literal(pos, RULE_COMMA) 670 | if p2 > pos: 671 | # There is a comma after the array element 672 | return v, p2 673 | 674 | # No comma after an array element is also fine in case 675 | # a closing paren follows which we don't consume. 676 | musthave_literal(pos, RULE_ARRAYEND) 677 | return v, pos 678 | 679 | def array(pos): 680 | v, pos = sequence(pos, RULE_ARRAY, arrayelements, RULE_ARRAYEND) 681 | return v[0], pos 682 | 683 | def kvpair(pos): 684 | k, p = anystring(pos) 685 | p = musthave_literal(p, RULE_EQUALS) 686 | v, p = value(p) 687 | p = musthave_literal(p, RULE_SEMICOLON) 688 | return (k, v), p 689 | 690 | def kvpairs(pos): 691 | return zero_or_more(pos, kvpair, RULE_DICTIONARYEND) 692 | 693 | def dictionary(pos): 694 | try: 695 | v, pos = sequence(pos, RULE_DICTIONARY, kvpairs, RULE_DICTIONARYEND) 696 | return dictionarytype(v[0]), pos 697 | except RuntimeError as exc: 698 | if str(exc).startswith('maximum recursion depth exceeded'): 699 | raise ParserError("Exceeded maximum recursion", pos) 700 | 701 | def quotedstring(pos): 702 | v, pos = named_rule(pos, RULE_QUOTEDSTRING) 703 | return unescape_str(v), pos 704 | 705 | def unquotedstring(pos): 706 | return named_rule(pos, RULE_UNQUOTEDSTRING) 707 | 708 | def named_rule(pos, rulenr): 709 | if rule_at(pos) != rulenr: 710 | raise ParserError('Expecting rule %d' % rulenr, pos) 711 | return text_at(pos), inc(pos) 712 | 713 | def parse_xcodeplist_tokens(pos): 714 | try: 715 | root = dictionary(pos) 716 | except ParserError as e: 717 | return False, e 718 | if root is None: 719 | return False, None 720 | 721 | v, pos = root 722 | if pos != len(tokenrules): 723 | return False, ParserError("there are still tokens left after parsing", pos) 724 | return True, v 725 | 726 | success, root = parse_xcodeplist_tokens(0) 727 | return success, root 728 | 729 | 730 | def parse_xcodeproject_xml(text, dictionarytype=dict): 731 | """We parse the XML format by transforming the document into a simplified plist format 732 | and handing this off to our plist parser. 733 | """ 734 | 735 | structures = { 736 | 'dict': '{};', 737 | 'array': '(),', 738 | } 739 | 740 | stack = ['dummy'] 741 | tokens = [] 742 | 743 | def emit(s): 744 | tokens.append(s) 745 | 746 | def emit_text(s): 747 | emit(quoted_string(s)) 748 | 749 | def emit_terminator(parent): 750 | structure_markers = structures.get(parent) 751 | if structure_markers is not None: 752 | emit(structure_markers[-1]) 753 | emit('\n') 754 | 755 | def iterxml(buf): 756 | try: 757 | for event, elem in ETree.iterparse(buf, events=('start', 'end')): 758 | tag = elem.tag 759 | if event == 'start': 760 | stack.append(tag) 761 | else: 762 | stack.pop() 763 | 764 | parent = stack[-1] 765 | 766 | if event == 'end': 767 | if tag == 'key': 768 | emit_text(elem.text) 769 | emit('=') 770 | elif tag == 'string': 771 | emit_text(elem.text) 772 | emit_terminator(parent) 773 | 774 | structure_markers = structures.get(tag) 775 | if structure_markers: 776 | if event == 'start': 777 | emit(structure_markers[0]) 778 | else: 779 | emit(structure_markers[1]) 780 | emit_terminator(parent) 781 | except ETree.ParseError as e: 782 | linenr, column, errortext = error_report_from('XML', text, text_type(e)) 783 | return {'error_column': column, 784 | 'error_line_number': linenr, 785 | 'error_text': errortext} 786 | return None 787 | 788 | errorparseinfo = iterxml(BytesIO(bytestr(text))) 789 | if errorparseinfo is not None: 790 | return None, errorparseinfo 791 | 792 | text = ''.join(tokens) 793 | root, parseinfo = parse_xcodeproject_plist(text, dictionarytype=dictionarytype) 794 | if root is not None: 795 | parseinfo['format'] = 'xml' 796 | return root, parseinfo 797 | 798 | 799 | # --------------------------------------------------------------- 800 | 801 | def unparse(root, format='xcode', projectname='', disable_comments=False, parseinfo=None): 802 | """Generate the content of a project.pbxproj. 803 | 804 | :type root: the root node of the tree. 805 | :type format: 'xcode', 'xml', 'json'. 806 | :type projectname: basename of the .xcodeproj. 807 | :type disable_comments: don't add comments after the gids. 808 | :type parseinfo: if you parsed the project you can pass this from the parse result. 809 | we use this to guess if comments should be recreated. 810 | :return: 811 | """ 812 | if root is None: 813 | raise ValueError("root is None") 814 | unparserclass = unparsers.get(format) 815 | if unparserclass is None: 816 | raise ValueError('format must be one of [%s]' % ', '.join(output_formats)) 817 | unparser = unparserclass(root) 818 | text = unparser.unparse(root, projectname=projectname, disable_comments=disable_comments, parseinfo=parseinfo) 819 | return bytestr(text) 820 | 821 | 822 | # noinspection PySetFunctionToLiteral 823 | class Unparser(object): 824 | """Creates the text representation from the parsed tree. 825 | """ 826 | 827 | header = '// !$*UTF8*$!\n' 828 | trailer = '\n' 829 | 830 | keys_without_comments = frozenset([ 831 | 'remoteGlobalIDString', 832 | 'TestTargetID', 833 | 'TargetAttributes' 834 | ]) 835 | 836 | pbx_names = { 837 | 'PBXProject': 'Project object', 838 | 'PBXTargetDependency': 'PBXTargetDependency', 839 | 'PBXBuildRule': 'PBXBuildRule', 840 | 'PBXContainerItemProxy': 'PBXContainerItemProxy', 841 | } 842 | 843 | commentpaths = frozenset(['PBXAggregateTarget.buildConfigurationList', 844 | 'PBXAggregateTarget.buildPhases', 845 | 'PBXAggregateTarget.dependencies', 846 | 'PBXAppleScriptBuildPhase.files', 847 | 'PBXBuildFile.fileRef', 848 | 'PBXContainerItemProxy.containerPortal', 849 | 'PBXCopyFilesBuildPhase.files', 850 | 'PBXFrameworksBuildPhase.files', 851 | 'PBXGroup.children', 852 | 'PBXHeadersBuildPhase.files', 853 | 'PBXLegacyTarget.buildConfigurationList', 854 | 'PBXNativeTarget.buildConfigurationList', 855 | 'PBXNativeTarget.buildPhases', 856 | 'PBXNativeTarget.buildRules', 857 | 'PBXNativeTarget.dependencies', 858 | 'PBXNativeTarget.productReference', 859 | 'PBXProject.buildConfigurationList', 860 | 'PBXProject.buildStyles', 861 | 'PBXProject.mainGroup', 862 | 'PBXProject.productRefGroup', 863 | 'PBXProject.projectReferences.ProductGroup', 864 | 'PBXProject.projectReferences.ProjectRef', 865 | 'PBXProject.targets', 866 | 'PBXReferenceProxy.remoteRef', 867 | 'PBXResourcesBuildPhase.files', 868 | 'PBXRezBuildPhase.files', 869 | 'PBXSourcesBuildPhase.files', 870 | 'PBXTargetDependency.target', 871 | 'PBXTargetDependency.targetProxy', 872 | 'PBXVariantGroup.children', 873 | 'XCBuildConfiguration.baseConfigurationReference', 874 | 'XCConfigurationList.buildConfigurations', 875 | 'XCVersionGroup.children', 876 | 'XCVersionGroup.currentVersion']) 877 | 878 | def __init__(self, root): 879 | if root is None: 880 | raise ValueError("root is None") 881 | self.objects = root.get('objects') 882 | self.keypath = [] 883 | self.open_section = None 884 | self.concise_mode = 0 885 | 886 | self.section_for_file = {} 887 | self.build_configuration_lists = {} 888 | self.gidcomments = {} 889 | 890 | self.outputbuffer = None 891 | self.projectname = None 892 | self.disable_comments = None 893 | self.version = None 894 | self.last_userhash = None 895 | 896 | def unparse(self, root, projectname='', disable_comments=False, parseinfo=None): 897 | if root is None: 898 | return None 899 | try: 900 | self.version = int(root.get('objectVersion')) 901 | except TypeError: 902 | return None 903 | self.outputbuffer = [] 904 | 905 | if projectname is not None: 906 | projectname = decode_utf8_or_sys(projectname) 907 | self.projectname = projectname 908 | 909 | self.disable_comments = disable_comments 910 | self.last_userhash = None 911 | self.set_comment_handling(disable_comments, parseinfo) 912 | 913 | self.create_lookup_tables() 914 | self.print_root(root, indent=0) 915 | return self.getoutput() 916 | 917 | def emit(self, s): 918 | self.outputbuffer.append(s) 919 | 920 | @staticmethod 921 | def getmember(obj, name): 922 | try: 923 | return obj.get(name) 924 | except AttributeError: 925 | return None 926 | 927 | def getoutput(self): 928 | return ''.join(self.outputbuffer) 929 | 930 | def set_comment_handling(self, disable_comments, parseinfo): 931 | if disable_comments is not None: 932 | self.disable_comments = disable_comments 933 | return 934 | 935 | sufficient_num_commented_nodes = 10 936 | if parseinfo: 937 | num_present = parseinfo.get('num_comments') 938 | if num_present is not None and num_present < sufficient_num_commented_nodes: 939 | self.disable_comments = True 940 | 941 | def print_root(self, root, indent=0): 942 | if not self.has_comments(): 943 | self.disable_comments = True 944 | if self.has_utf8_header(): 945 | self.emit(self.header) 946 | self.emit_node(root, indent) 947 | self.emit(self.trailer) 948 | 949 | def create_lookup_tables(self): 950 | """When we generate comments we'd get quadratic behaviour 951 | to find file sections and buildconfigurations. 952 | Build lookup tables to avoid this. 953 | """ 954 | for obj in self.objects.values(): 955 | files = self.getmember(obj, 'files') 956 | if isinstance(files, list): 957 | name = self.get_name(obj) 958 | if name is None: 959 | name = self.buildphasename(self.getmember(obj, 'isa')) 960 | for f in files: 961 | self.section_for_file[f] = name 962 | bcl = self.getmember(obj, 'buildConfigurationList') 963 | if bcl is not None: 964 | self.build_configuration_lists[bcl] = obj 965 | 966 | @staticmethod 967 | def buildphasename(name): 968 | if name is None: 969 | return None 970 | start = 'PBX' 971 | end = 'BuildPhase' 972 | if name.startswith(start) and name.endswith(end): 973 | return name[len(start):-len(end)] 974 | return None 975 | 976 | def supress_comment(self): 977 | if len(self.keypath) >= 1 and self.keypath[-1] in self.keys_without_comments: 978 | return True 979 | if len(self.keypath) >= 2 and self.keypath[-2] in self.keys_without_comments: 980 | return True 981 | return False 982 | 983 | def comment_for_obj(self, obj): 984 | if self.disable_comments: 985 | return None 986 | 987 | if self.supress_comment(): 988 | return None 989 | 990 | isa = self.getmember(obj, 'isa') 991 | return (self.pbx_names.get(isa) 992 | or self.get_name(obj) 993 | or self.buildphasename(isa) 994 | or self.name_for_object(obj)) 995 | 996 | def build_configuration(self, bcuuid): 997 | obj = self.objects.get(bcuuid) 998 | if obj is None: 999 | return None 1000 | if obj.get('isa') != 'XCConfigurationList': 1001 | return None 1002 | 1003 | obj = self.build_configuration_lists.get(bcuuid) 1004 | if obj is not None: 1005 | isa = obj.get('isa') 1006 | name = self.get_name(obj) or self.name_of_first_target(obj) 1007 | if isa == 'PBXProject': 1008 | name = self.projectname or name.strip() 1009 | name = self.transform_to_nfd(name) 1010 | comment = 'Build configuration list' 1011 | if self.has_build_configuration_list_detail(): 1012 | comment += ' for %s "%s"' % (isa, name) 1013 | return comment 1014 | return None 1015 | 1016 | def name_of_first_target(self, obj): 1017 | targets = self.getmember(obj, 'targets') 1018 | if isinstance(targets, list): 1019 | for uuid in targets: 1020 | name = self.name_for_object(self.objects.get(uuid)) 1021 | if name is not None: 1022 | return name 1023 | return None 1024 | 1025 | def transform_to_nfd(self, text): 1026 | if text is None: 1027 | return None 1028 | if not self.has_nfd_comments(): 1029 | return text 1030 | return unicodedata.normalize('NFD', text) 1031 | 1032 | def get_name(self, obj): 1033 | return self.transform_to_nfd(self.getmember(obj, 'name')) 1034 | 1035 | def name_for_object(self, obj): 1036 | if obj is None or not isinstance(obj, dict): 1037 | return None 1038 | return (self.get_name(obj) 1039 | or self.getmember(obj, 'path') 1040 | or self.name_for_object(self.objects.get(self.getmember(obj, 'fileRef'))) 1041 | or self.name_of_first_target(obj)) 1042 | 1043 | def comment_for_value(self, v): 1044 | if not is_global_id(v): 1045 | return None 1046 | 1047 | if not self.valid_comment_keypath(): 1048 | return None 1049 | 1050 | comment = self.gidcomments.get(v) 1051 | if comment is not None: 1052 | return comment 1053 | 1054 | comment = None 1055 | buildconf = self.build_configuration(v) 1056 | if buildconf is not None: 1057 | comment = buildconf 1058 | else: 1059 | obj = self.objects.get(v) 1060 | if obj is not None and isinstance(obj, dict): 1061 | comment = self.comment_for_obj(obj) 1062 | section = self.section_for_file.get(v) 1063 | if section is not None: 1064 | comment = "%s in %s" % (comment or '(null)', section) 1065 | 1066 | self.gidcomments[v] = comment or '' 1067 | return comment 1068 | 1069 | def valid_comment_keypath(self): 1070 | """Adding a comment to every value that looks like a global id 1071 | is incorrect in the improbable case that e.g. a file is named exactly as 1072 | a global id that exists in the same project. 1073 | """ 1074 | k = self.keypath 1075 | if k[0] == 'objects': 1076 | if len(k) == 2: 1077 | return True 1078 | elif len(k) >= 3: 1079 | path = [self.get_isa(self.objects.get(x)) or x for x in k[1:]] 1080 | kp = '.'.join(path) 1081 | return kp in self.commentpaths 1082 | # The only other commented keypath is the rootObject. 1083 | return k == ['rootObject'] 1084 | 1085 | def in_fileobj(self, obj): 1086 | return self.getmember(obj, 'isa') in ['PBXBuildFile', 'PBXFileReference'] 1087 | 1088 | def get_isa(self, obj): 1089 | if isinstance(obj, dict): 1090 | return self.getmember(obj, 'isa') 1091 | return None 1092 | 1093 | def begin_section(self, obj): 1094 | isa = self.get_isa(obj) 1095 | if isa != self.open_section: 1096 | self.close_section() 1097 | self.emit("\n/* Begin %s section */\n" % isa) 1098 | self.open_section = isa 1099 | return True 1100 | return False 1101 | 1102 | def close_section(self): 1103 | if self.open_section: 1104 | self.emit('/* End %s section */\n' % self.open_section) 1105 | self.open_section = None 1106 | 1107 | # ----------------------------------------------------------- 1108 | # Xcode format version dependent checks 1109 | 1110 | def has_utf8_header(self): 1111 | return self.version > 30 1112 | 1113 | def has_userhash_comments(self): 1114 | return 32 < self.version < 40 1115 | 1116 | def has_ungrouped_objects_sort(self): 1117 | return self.version < 40 1118 | 1119 | def has_leading_isa(self): 1120 | return self.version >= 40 1121 | 1122 | def has_concise_format(self): 1123 | return self.version >= 40 1124 | 1125 | def has_comments(self): 1126 | return self.version >= 40 1127 | 1128 | def has_build_configuration_list_detail(self): 1129 | return self.version >= 41 1130 | 1131 | def has_nfd_comments(self): 1132 | """There is a UTF-8 inconsistency in Xcode starting with object version 46. 1133 | Although Xcode uses the composed Unicode form (NFC) almost everywhere, 1134 | some comments are encoded in the decomposed Unicode form (NFD) which 1135 | is probably because the HFS Plus file system stores filenames in NFD. 1136 | """ 1137 | return self.version >= 46 1138 | 1139 | # ----------------------------------------------------------- 1140 | 1141 | @staticmethod 1142 | def objects_sortkey(kv): 1143 | """The 'objects' dict are sorted by isa first. This way we get the 1144 | sections, e.g. /* Begin PBXBuildFile section */. 1145 | The secondary sort key is the gid which creates the proper 1146 | order within the sections. 1147 | """ 1148 | try: 1149 | return kv[1].get('isa'), kv[0] 1150 | except AttributeError: 1151 | return kv[0] 1152 | 1153 | @staticmethod 1154 | def ungrouped_objects_sortkey(kv): 1155 | return kv[0] 1156 | 1157 | def sorted_items(self, dictionary): 1158 | if self.keypath == ['objects']: 1159 | # This is the 'objects' dict which we sort by (isa, gid) into sections. 1160 | if self.has_ungrouped_objects_sort(): 1161 | sortkey = self.ungrouped_objects_sortkey 1162 | else: 1163 | sortkey = self.objects_sortkey 1164 | else: 1165 | sortkey = None 1166 | if self.has_leading_isa(): 1167 | isa = dictionary.get('isa') 1168 | if isa is not None: 1169 | items = [('isa', isa)] 1170 | items.extend(sorted(x for x in dictionary.items() if x[0] != 'isa')) 1171 | return items 1172 | return sorted(dictionary.items(), key=sortkey) 1173 | 1174 | def emit_value(self, v): 1175 | v = quoted_string(v) 1176 | self.emit(v) 1177 | comment = self.comment_for_value(v) 1178 | if comment is not None: 1179 | if not self.disable_comments: 1180 | self.emit_comment(comment) 1181 | 1182 | def emit_kvpair(self, k, v, indent): 1183 | self.emit_value(k) 1184 | self.emit(' = ') 1185 | self.emit_node(v, 0 if self.concise() else indent) 1186 | 1187 | def concise(self): 1188 | return self.concise_mode and self.has_concise_format() 1189 | 1190 | def emit_comment(self, comment): 1191 | if comment: 1192 | self.emit(' /* ') 1193 | self.emit(comment) 1194 | self.emit(' */') 1195 | 1196 | def emit_prologue(self): 1197 | if not self.concise(): 1198 | self.emit('\n') 1199 | 1200 | def emit_indent(self, indent): 1201 | self.emit('\t' * indent) 1202 | 1203 | def emit_leading_separator(self, indent): 1204 | self.emit_indent(0 if self.concise() else indent) 1205 | 1206 | def emit_trailing_separator(self): 1207 | self.emit(' ' if self.concise() else '\n') 1208 | 1209 | def emit_userhash_comments(self, k): 1210 | if not self.has_userhash_comments(): 1211 | return 1212 | if len(self.keypath) != 1: 1213 | return 1214 | if self.keypath[0] != 'objects': 1215 | return 1216 | if self.last_userhash is None: 1217 | self.last_userhash = k 1218 | return 1219 | a = self.last_userhash[:2] 1220 | b = k[:2] 1221 | if a != b: 1222 | self.emit_userhash_range(a) 1223 | self.emit_userhash_range(b) 1224 | self.last_userhash = b 1225 | 1226 | def emit_userhash_range(self, userhash): 1227 | for i in range(5): 1228 | self.emit('//%s%d\n' % (userhash, i)) 1229 | 1230 | def emit_map(self, node, indent): 1231 | self.emit('{') 1232 | self.emit_prologue() 1233 | began_sections = False 1234 | sections = self.keypath == ['objects'] and not self.disable_comments 1235 | for k, v in self.sorted_items(node): 1236 | self.emit_userhash_comments(k) 1237 | self.keypath.append(k) 1238 | began_sections = sections and (self.begin_section(v) or began_sections) 1239 | self.emit_leading_separator(indent + 1) 1240 | self.emit_kvpair(k, v, indent + 1) 1241 | self.emit(';') 1242 | self.emit_trailing_separator() 1243 | self.keypath.pop() 1244 | if began_sections: 1245 | self.close_section() 1246 | self.emit_leading_separator(indent) 1247 | self.emit('}') 1248 | 1249 | def emit_list(self, node, indent): 1250 | self.emit('(') 1251 | self.emit_prologue() 1252 | for v in node: 1253 | self.emit_leading_separator(indent + 1) 1254 | self.emit_node(v, indent + 1) 1255 | self.emit(',') 1256 | self.emit_trailing_separator() 1257 | self.emit_leading_separator(indent) 1258 | self.emit(')') 1259 | 1260 | def emit_node(self, node, indent=0): 1261 | if isinstance(node, dict): 1262 | concise_output = self.in_fileobj(node) 1263 | if concise_output: 1264 | self.concise_mode += 1 1265 | self.emit_map(node, indent) 1266 | if concise_output: 1267 | self.concise_mode -= 1 1268 | elif isinstance(node, (list, tuple)): 1269 | self.emit_list(node, indent) 1270 | else: 1271 | self.emit_value(node) 1272 | 1273 | 1274 | class XMLUnparser(Unparser): 1275 | header = """ 1276 | 1277 | 1278 | """ 1279 | trailer = '\n' 1280 | 1281 | def __init__(self, root): 1282 | super(XMLUnparser, self).__init__(root) 1283 | self.disable_comments = True 1284 | 1285 | def has_concise_format(self): 1286 | return False 1287 | 1288 | def has_leading_isa(self): 1289 | return False 1290 | 1291 | @staticmethod 1292 | def escape_tag_entities(data): 1293 | # For clarity we are not going to import xml.sax.saxutils 1294 | # for these three lines. 1295 | data = data.replace("&", "&") 1296 | data = data.replace(">", ">") 1297 | data = data.replace("<", "<") 1298 | return data 1299 | 1300 | def emit_value(self, v): 1301 | self.emit('') 1302 | self.emit(self.escape_tag_entities(v)) 1303 | self.emit('') 1304 | 1305 | def emit_kvpair(self, k, v, indent): 1306 | self.emit('') 1307 | self.emit(self.escape_tag_entities(k)) 1308 | self.emit('') 1309 | self.emit('\n') 1310 | self.emit_node(v, indent) 1311 | 1312 | def emit_map(self, node, indent): 1313 | if not node: 1314 | self.emit('') 1315 | return 1316 | self.emit('') 1317 | self.emit('\n') 1318 | for k, v in self.sorted_items(node): 1319 | self.emit_indent(indent + 1) 1320 | self.emit_kvpair(k, v, indent + 1) 1321 | 1322 | self.emit_indent(indent) 1323 | self.emit('') 1324 | 1325 | def emit_list(self, node, indent): 1326 | if len(node) == 0: 1327 | self.emit('') 1328 | return 1329 | self.emit('') 1330 | self.emit('\n') 1331 | for v in node: 1332 | self.emit_node(v, indent + 1) 1333 | self.emit_leading_separator(indent) 1334 | self.emit('') 1335 | 1336 | def emit_node(self, node, indent=0): 1337 | self.emit_indent(indent) 1338 | super(XMLUnparser, self).emit_node(node, indent=indent) 1339 | self.emit('\n') 1340 | 1341 | 1342 | class JSONUnparser(Unparser): 1343 | def __init__(self, root): 1344 | super(JSONUnparser, self).__init__(root) 1345 | self.disable_comments = True 1346 | 1347 | def unparse(self, root, projectname='', disable_comments=False, parseinfo=None): 1348 | try: 1349 | return json.dumps(root, sort_keys=True, 1350 | indent=2, 1351 | separators=(',', ':')) 1352 | except ValueError: 1353 | return None 1354 | 1355 | 1356 | # --------------------------------------------------------------- 1357 | 1358 | unparsers = OrderedDict([ 1359 | ('xcode', Unparser), 1360 | ('xml', XMLUnparser), 1361 | ('json', JSONUnparser)]) 1362 | output_formats = unparsers.keys() 1363 | 1364 | # --------------------------------------------------------------- 1365 | 1366 | 1367 | class UniqueXcodeIDGenerator(object): 1368 | """This class generates global ids in a schema 1369 | similar to the one Xcode uses. 1370 | The optional keyword arguments allow a deterministic 1371 | generation. 1372 | """ 1373 | 1374 | AbsoluteTimeIntervalSince1970 = 978307200 1375 | 1376 | def __init__(self, username=None, pid=None, refdatefunc=None): 1377 | if refdatefunc is None: 1378 | refdatefunc = self.reftime 1379 | self.initialseq = 0 1380 | self.lasttime = 0 1381 | self.refdatefunc = refdatefunc 1382 | if pid is None: 1383 | pid = os.getpid() 1384 | self.rndgen = random.Random(xor((pid << 16), refdatefunc())) 1385 | self.userhash = self.user_hash(username) 1386 | self.pidbyte = pid & 0xff 1387 | self.randomconst = self.rndgen.randint(0, (2 ** 31) - 1) & 0x00ffffff 1388 | self.randomseq = self.rndgen.randint(0, (2 ** 31) - 1) & 0xffff 1389 | 1390 | def generate(self): 1391 | refdate = self.refdatefunc() 1392 | self.randomseq += 1 1393 | self.lasttime = 0 1394 | if refdate > self.lasttime: 1395 | self.initialseq = self.randomseq 1396 | self.lasttime = refdate 1397 | else: 1398 | if self.randomseq == self.initialseq: 1399 | self.lasttime += 1 1400 | refdate = self.lasttime 1401 | 1402 | return (self.hexbyte(self.userhash) + self.hexbyte(self.pidbyte) 1403 | + self.big_endian_hex(self.randomseq, 2) 1404 | + self.big_endian_hex(refdate, 4) 1405 | + self.big_endian_hex(self.randomconst, 4)) 1406 | 1407 | @staticmethod 1408 | def user_hash(username=None): 1409 | userhash = 0 1410 | hashvalue = 0 1411 | if username is None: 1412 | username = os.getlogin() 1413 | for inp in username: 1414 | cc = UniqueXcodeIDGenerator.five_bit_hash(ord(inp)) 1415 | if hashvalue != 0: 1416 | cc = ((cc << hashvalue) >> 8) | (cc << hashvalue) 1417 | hashvalue = hashvalue + 5 & 7 1418 | userhash = xor(userhash, cc) 1419 | return userhash & 255 1420 | 1421 | @staticmethod 1422 | def five_bit_hash(c): 1423 | if ord('A') <= c <= ord('Z'): 1424 | return c - ord('A') 1425 | elif ord('a') <= c <= ord('z'): 1426 | return c - ord('a') 1427 | elif ord('0') <= c <= ord('9'): 1428 | return ord('Z') - ord('A') + 1 + ((c - ord('0')) % 5) 1429 | else: 1430 | return 31 1431 | 1432 | @staticmethod 1433 | def hexbyte(c): 1434 | return '%02X' % c 1435 | 1436 | @staticmethod 1437 | def big_endian_hex(v, size): 1438 | lst = [] 1439 | for _ in range(size): 1440 | lst.append(v & 0xff) 1441 | v >>= 8 1442 | lst.reverse() 1443 | return ''.join(UniqueXcodeIDGenerator.hexbyte(c) for c in lst) 1444 | 1445 | @staticmethod 1446 | def big_endian_number(hexstring): 1447 | sum = 0 1448 | while hexstring: 1449 | sum *= 256 1450 | byte = hexstring[:2] 1451 | sum += int(byte, 16) 1452 | hexstring = hexstring[2:] 1453 | return sum 1454 | 1455 | @staticmethod 1456 | def reftime(t=None): 1457 | if t is None: 1458 | t = time.time() 1459 | return int(t - UniqueXcodeIDGenerator.AbsoluteTimeIntervalSince1970) 1460 | 1461 | @staticmethod 1462 | def reftime_to_epoch(t): 1463 | return t + UniqueXcodeIDGenerator.AbsoluteTimeIntervalSince1970 1464 | 1465 | 1466 | class IDGeneratorClock(object): 1467 | def __init__(self, seconds): 1468 | self.seconds = seconds 1469 | 1470 | def tick(self): 1471 | self.seconds += 1 1472 | 1473 | def getseconds(self): 1474 | return self.seconds 1475 | 1476 | 1477 | def datetime_from_utc(refdate): 1478 | return datetime.datetime.strptime(refdate, '%Y-%m-%dT%H:%M:%SZ') 1479 | 1480 | 1481 | def generate_gids(num, username=None, pid=None, refdate=None): 1482 | if refdate is not None: 1483 | dt = datetime_from_utc(refdate) 1484 | t = time.mktime(dt.timetuple()) 1485 | else: 1486 | t = None 1487 | secs = UniqueXcodeIDGenerator.reftime(t) 1488 | 1489 | clock = IDGeneratorClock(secs) 1490 | generator = UniqueXcodeIDGenerator(username=username, pid=pid, refdatefunc=clock.getseconds) 1491 | for i in range(num): 1492 | gid = generator.generate() 1493 | # We can only generate 65536 different gids for the same second, 1494 | # then we go on to the next second. 1495 | yield gid 1496 | if i & 0xffff == 0xffff: 1497 | clock.tick() 1498 | 1499 | 1500 | def iprint(category, *args, **kwargs): 1501 | if category in args_info: 1502 | print(*args, **kwargs) 1503 | 1504 | 1505 | def dprint(category, *args, **kwargs): 1506 | if category in args_debug: 1507 | print(*args, **kwargs) 1508 | 1509 | 1510 | def outline(s, fp=None): 1511 | if fp is None: 1512 | fp = sys.stdout 1513 | fp.write(unistr(s + '\n')) 1514 | 1515 | reportmessage = outline 1516 | 1517 | def reporterror(s, fp=None): 1518 | if fp is None: 1519 | fp = sys.stderr 1520 | reportmessage(s, fp=fp) 1521 | 1522 | reportwarning = reporterror 1523 | 1524 | def print_gids(num, username=None, pid=None, refdate=None): 1525 | for g in generate_gids(num, username=username, pid=pid, refdate=refdate): 1526 | outline(unistr(g)) 1527 | return OK 1528 | 1529 | 1530 | def comment_for_gid(giddict, gid): 1531 | if isinstance(giddict, dict): 1532 | comment = giddict.get(gid) 1533 | if comment is not None: 1534 | return comment 1535 | return None 1536 | 1537 | 1538 | def gidfields(giddict, gid): 1539 | parts = gid[:2], gid[2:4], gid[4:8], gid[8:16], gid[16:24] 1540 | parts = [UniqueXcodeIDGenerator.big_endian_number(x) for x in parts] 1541 | user, pid, seq, date, randomconst = parts 1542 | date = UniqueXcodeIDGenerator.reftime_to_epoch(date) 1543 | date = datetime.datetime.utcfromtimestamp(date) 1544 | date = date.isoformat() + 'Z' 1545 | comment = comment_for_gid(giddict, gid) 1546 | d = OrderedDict([ 1547 | ('date', date), 1548 | ('user', user), 1549 | ('pid', pid), 1550 | ('random', randomconst), 1551 | ('seq', seq), 1552 | ('gid', gid)]) 1553 | if comment: 1554 | d['comment'] = comment 1555 | return d 1556 | 1557 | 1558 | def gidsplit(gidseq, format='text', sort=False, buf=None): 1559 | """This function prints a columnar JSON representation of the splitted gids 1560 | with the date in front so one can sort and extract data with the 1561 | usual command line tools as well as read the output as JSON. 1562 | """ 1563 | gids = [g for g in gidseq if is_global_id(g)] 1564 | if not gids: 1565 | return 1566 | 1567 | out = buf 1568 | if out is None: 1569 | out = sys.stdout 1570 | 1571 | def uniwrite(text): 1572 | out.write(unistr(text)) 1573 | 1574 | 1575 | if sort: 1576 | gids.sort() 1577 | entries = [gidfields(gidseq, gid) for gid in gids] 1578 | 1579 | if format == 'json': 1580 | root = {'gids': entries} 1581 | uniwrite(json.dumps(root, sort_keys=True, indent=2, separators=(',', ':'))) 1582 | uniwrite('\n') 1583 | elif format == 'text': 1584 | formatstrings = {"date": "%s", "user": "%3d", "pid": "%3d", "random": "%10d", 1585 | "seq": "%5d", "gid": "%24s", "comment": "%s"} 1586 | jsondumps = json.JSONEncoder().encode 1587 | for d in entries: 1588 | comment = d.get('comment') 1589 | if comment is not None: 1590 | d['comment'] = jsondumps(comment) 1591 | uniwrite(' '.join(formatstrings[k] % v for k, v in d.items())) 1592 | uniwrite('\n') 1593 | return OK 1594 | 1595 | 1596 | def giddump(args, parser): 1597 | filenames = args.filename 1598 | if len(filenames) > 1: 1599 | parser.error('Please specify no more than one filename to giddump') 1600 | # The return is only reached with a test parser from the unit tests. 1601 | return 1 1602 | 1603 | filename = (filenames and filenames[0]) or STDIN 1604 | xcodeproj = data_from_filename(filename) 1605 | root, parseinfo = parse(xcodeproj, parsertype=args.parser) 1606 | report_parse_status(root, parseinfo, filename=filename) 1607 | if root is None: 1608 | return PARSING_FAILED 1609 | 1610 | projectname = projectname_from_args(args, parser, filename, parseinfo.get('projectname')) 1611 | 1612 | unparser = Unparser(root) 1613 | # Run the unparser to get the table of gidcomments 1614 | _ = unparser.unparse(root, projectname=projectname) 1615 | 1616 | if args.outputfile is not None and args.outputfile != '-': 1617 | destfilename = args.outputfile 1618 | with codecs.open(destfilename, 'w', encoding='utf-8') as fp: 1619 | gidsplit(unparser.gidcomments, format=args.gid_format, sort=True, buf=fp) 1620 | else: 1621 | gidsplit(unparser.gidcomments, format=args.gid_format, sort=True) 1622 | 1623 | return OK 1624 | 1625 | # ---------------------------------------------------------------------- 1626 | 1627 | 1628 | def find_projectfiles(rootdir): 1629 | """Iterates recursively over rootdir and yields 1630 | every filename that looks like a credible project.pbxproj. 1631 | """ 1632 | validfirstchars = {'/', # usual header for the plist format: // !$*UTF8*$! 1633 | '<', # 1634 | '{' # plist format without the UTF8 header 1635 | } 1636 | for path, dirs, files in os.walk(rootdir): 1637 | for name in files: 1638 | if name != PBXPROJNAME: 1639 | continue 1640 | filename = os.path.join(path, name) 1641 | with codecs.open(filename, 'r', encoding='utf-8') as f: 1642 | c = f.read(1) 1643 | if c in validfirstchars: 1644 | yield filename 1645 | 1646 | 1647 | def unilines(text): 1648 | return unistr(text).splitlines(True) 1649 | 1650 | 1651 | def print_unified_diff(a, b, fp=None, **kwargs): 1652 | if fp is None: 1653 | fp = sys.stdout 1654 | for line in difflib.unified_diff(unilines(a), unilines(b), **kwargs): 1655 | fp.write(unistr(line)) 1656 | if not line.endswith('\n'): 1657 | fp.write(unistr('\n')) 1658 | 1659 | 1660 | def timestamp(): 1661 | return 'projer' + str(time.time()).replace('.', '_') 1662 | 1663 | 1664 | def print_diff(a, b, difftype='unified', filename=None, fp=None): 1665 | 1666 | def write_tempfile(data, **kwargs): 1667 | fd, path = tempfile.mkstemp(**kwargs) 1668 | try: 1669 | os.write(fd, bytestr(data)) 1670 | finally: 1671 | os.close(fd) 1672 | return path 1673 | 1674 | if difftype == 'unified': 1675 | print_unified_diff(a, b, fromfile=filename, tofile='', n=3, fp=fp) 1676 | elif difftype == 'html': 1677 | htmldiffer = difflib.HtmlDiff(tabsize=4) 1678 | html = htmldiffer.make_file(unilines(a), unilines(b), 1679 | fromdesc=filename, 1680 | todesc='', 1681 | context=True, numlines=2) 1682 | htmlfilename = write_tempfile(html, suffix='.html', prefix=timestamp()) 1683 | reporterror('\nopen "%s"' % htmlfilename, fp=fp) 1684 | elif difftype == 'opendiff': 1685 | outfilename = write_tempfile(b, suffix='.pbxproj', prefix=timestamp()) 1686 | reporterror('\nopendiff "%s" "%s"' % (filename, outfilename), fp=fp) 1687 | else: 1688 | raise ValueError("Unknown difftype: '%s'" % difftype) 1689 | 1690 | 1691 | def farthest_parseinfo(parseinfo): 1692 | """As we might have tried several attempts of parsing different formats 1693 | we have several error reports from the respective parsers. 1694 | The parser that made it the farthest (line, column) probably 1695 | matched the format but failed anyway. 1696 | The error report of this least unsuccessful parser is the only 1697 | one being reported. 1698 | """ 1699 | line = parseinfo.get('error_line_number', -1) 1700 | column = parseinfo.get('error_column', -1) 1701 | prev = parseinfo.get('prev_parseinfo') 1702 | if prev is None: 1703 | return line, column, parseinfo 1704 | prevline, prevcolumn, prev = farthest_parseinfo(prev) 1705 | if (prevline, prevcolumn) > (line, column): 1706 | return prevline, prevcolumn, prev 1707 | else: 1708 | return line, column, parseinfo 1709 | 1710 | 1711 | def report_parse_status(root, parseinfo, filename=None, fp=None): 1712 | # Report warnings after a successful parse. 1713 | if isinstance(parseinfo, dict): 1714 | _warnings = parseinfo.get('warnings', []) 1715 | for w in _warnings: 1716 | reportwarning(w, fp=fp) 1717 | 1718 | iprint(INFO_TIME, "Parse time:", parseinfo.get('parsetime')) 1719 | 1720 | if root is not None: 1721 | # We have a parse tree, no errors to report here. 1722 | return 1723 | 1724 | if filename == STDIN: 1725 | filename = '' 1726 | else: 1727 | filename = '%s' % filename 1728 | line, column, parseinfo = farthest_parseinfo(parseinfo) 1729 | parseinfo = parseinfo.copy() 1730 | parseinfo['filename'] = filename 1731 | reporterror('File %(filename)s, line %(error_line_number)d, column %(error_column)d' % parseinfo, fp=fp) 1732 | reporterror(parseinfo.get('error_text'), fp=fp) 1733 | 1734 | 1735 | def data_from_filename(filename): 1736 | if filename == STDIN: 1737 | return sys.stdin.read() 1738 | else: 1739 | with open(filename, 'rb') as f: 1740 | return f.read() 1741 | 1742 | 1743 | def projectname_from_args(args, parser, filename, prjname=None): 1744 | if filename == STDIN: 1745 | projectname = args.projectname 1746 | if projectname is None: 1747 | if prjname is not None: 1748 | reportwarning('Warning: We needed to guess "%s" as project name which might not be correct.\n' 1749 | ' It would be preferable if you could supply the definite project name using --projectname when you read from stdin.' % prjname) 1750 | projectname = prjname 1751 | else: 1752 | parser.error('When we get the project via stdin (-) you must specify the project name with --projectname') 1753 | else: 1754 | projectname = projectname_for_path(filename) 1755 | return projectname 1756 | 1757 | 1758 | def lint(args, parser): 1759 | filenames = args.filename 1760 | exit_code = OK 1761 | if not filenames: 1762 | filenames = [STDIN] 1763 | 1764 | for filename in filenames: 1765 | xcodeproj = data_from_filename(filename) 1766 | root, parseinfo = parse(xcodeproj, format=None, parsertype=args.parser) 1767 | report_parse_status(root, parseinfo, filename=filename) 1768 | if root is None: 1769 | exit_code = max(exit_code, LINT_FAILED) 1770 | continue 1771 | 1772 | if parseinfo['format'] not in ['xcode', 'xml']: 1773 | reportmessage('The project file "%s" is in %s which is nothing that Xcode can read.' % (filename, parseinfo['format'])) 1774 | exit_code = max(exit_code, LINT_FAILED) 1775 | continue 1776 | 1777 | if parseinfo['format'] == 'xml': 1778 | reportmessage('The project file "%s" is in XML which is a clearly a failed lint.' % filename) 1779 | exit_code = max(exit_code, LINT_FAILED) 1780 | continue 1781 | 1782 | projectname = projectname_from_args(args, parser, filename, parseinfo.get('projectname')) 1783 | proj = unparse(root, projectname=projectname) 1784 | if xcodeproj != proj: 1785 | exit_code = max(exit_code, LINT_DIFFERENCES) 1786 | print_unified_diff(xcodeproj, proj, fromfile=filename) 1787 | return exit_code 1788 | 1789 | 1790 | def convert(args, parser): 1791 | filenames = args.filename 1792 | if len(filenames) > 1: 1793 | parser.error('Please specify no more than one filename to convert') 1794 | # The return is only reached with a test parser from the unit tests. 1795 | return 1 1796 | 1797 | filename = (filenames and filenames[0]) or STDIN 1798 | xcodeproj = data_from_filename(filename) 1799 | root, parseinfo = parse(xcodeproj, parsertype=args.parser) 1800 | report_parse_status(root, parseinfo, filename=filename) 1801 | if root is None: 1802 | return PARSING_FAILED 1803 | 1804 | # Set objectVersion if requested 1805 | if args.objectversion != 'same': 1806 | if args.objectversion == 'latest': 1807 | version = text_type(LATEST_OBJECT_VERSION) 1808 | else: 1809 | version = args.objectversion 1810 | root['objectVersion'] = version 1811 | 1812 | projectname = projectname_from_args(args, parser, filename, parseinfo.get('projectname')) 1813 | proj = unparse(root, 1814 | format=args.convert, 1815 | projectname=projectname, 1816 | disable_comments=args.comments == 'no', 1817 | parseinfo=parseinfo) 1818 | 1819 | if args.outputfile is not None: 1820 | destfilename = args.outputfile 1821 | elif filename == STDIN: 1822 | destfilename = STDOUT 1823 | else: 1824 | destfilename = filename 1825 | 1826 | if destfilename == STDOUT: 1827 | buf = sys.stdout 1828 | try: 1829 | numbytes = buf.write(proj) 1830 | except TypeError: 1831 | # We are probably writing to a io.StringIO here. 1832 | numbytes = len(proj) 1833 | buf.write(unistr(proj)) 1834 | else: 1835 | with open(destfilename, 'wb') as f: 1836 | numbytes = f.write(proj) 1837 | 1838 | if numbytes is not None and numbytes != len(proj): 1839 | reporterror('Incomplete output, only %d of %d bytes written' % (numbytes, len(proj))) 1840 | return CONVERT_OUTPUT_FAILED 1841 | 1842 | return OK 1843 | 1844 | 1845 | # ---------------------------------------------------------------------- 1846 | 1847 | def cmdline_parser(parserclass=argparse.ArgumentParser): 1848 | """The option to change the parserclass is used for testing the 1849 | command line options. 1850 | """ 1851 | parser = parserclass(description='Convert Xcode project files into different formats.') 1852 | parser.add_argument('-o', '--outputfile', help='output filename or - for stdout') 1853 | parser.add_argument('--projectname', action='store', help='the directory name without the .xcodeproj, necessary for stdin input') 1854 | parser.add_argument('--parser', choices=['normal', 'fast', 'classic'], default='normal') 1855 | # The info and debug options were inspired by rsync. 1856 | parser.add_argument('--info', help='fine-grained informational verbosity') 1857 | parser.add_argument('--debug', help='fine-grained debug verbosity') 1858 | parser.add_argument('--verbose', '-v', action='count') 1859 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) 1860 | 1861 | group = parser.add_argument_group('Convert file formats') 1862 | group.add_argument('-c', '--convert', choices=output_formats, help='convert into a specific format') 1863 | group.add_argument('--objectversion', action='store', default='same', help='output version of plist, e.g.: 30, 46, latest, same') 1864 | group.add_argument('--comments', choices=['yes', 'no', 'same'], default='yes', 1865 | help='only meaningful for plist output, same means include if input file was commented.') 1866 | 1867 | lintgroup = parser.add_argument_group('Lint file formats') 1868 | lintgroup.add_argument('--lint', action='store_true', help='checks if the files are in properly commented plist format') 1869 | 1870 | gidgroup = parser.add_argument_group('Global ids') 1871 | gidgroup.add_argument('--gid', nargs='?', const=1, type=int, default=None, metavar='NUM', help='number of global ids to generate') 1872 | gidgroup.add_argument('--gidsplit', nargs='+', default=None, metavar='GID_TO_INTERPRET', help='transform global ids into readable components') 1873 | gidgroup.add_argument('--giddump', action='store_true', help='representation of commented gids, for software archeologists') 1874 | gidgroup.add_argument('--gid-pid', type=int, default=None, help='pid for the global id generator') 1875 | gidgroup.add_argument('--gid-user', default=None, help='username for the global id generator') 1876 | gidgroup.add_argument('--gid-date', default=None, help='base date for the global id generator, e.g. 2007-01-09T16:41:00Z') 1877 | gidgroup.add_argument('--gid-format', choices=['json', 'text'], default='text', help='output format for gidsplit and giddump') 1878 | 1879 | parser.add_argument('filename', nargs='*', help='input filename') 1880 | 1881 | return parser 1882 | 1883 | 1884 | def set_logging_parameters(args): 1885 | for attr, catset, defaults in ( 1886 | ('info', args_info, INFO_ALL), 1887 | ('debug', args_debug, DEBUG_ALL)): 1888 | option = getattr(args, attr) 1889 | if option: 1890 | catset.update(x.lower() for x in option.split(',')) 1891 | if 'all' in catset: 1892 | catset.update(defaults) 1893 | 1894 | if args.verbose is not None: 1895 | for level in sorted(verbose_categories): 1896 | if args.verbose >= level: 1897 | args_info.update(verbose_categories[level]) 1898 | 1899 | 1900 | def run_with_args(args, parser): 1901 | set_logging_parameters(args) 1902 | 1903 | dprint(DEBUG_OPTIONS, args) 1904 | 1905 | num_actions = 0 1906 | actions = 'convert lint gid gidsplit giddump'.split() 1907 | for act in actions: 1908 | if getattr(args, act): 1909 | num_actions += 1 1910 | 1911 | if num_actions != 1: 1912 | parser.error('Please specify exactly one of the options %s.' % ', '.join('--' + x for x in actions)) 1913 | 1914 | ret = 0 1915 | if args.gid is not None: 1916 | ret = print_gids(args.gid, username=args.gid_user, pid=args.gid_pid, refdate=args.gid_date) 1917 | elif args.gidsplit: 1918 | ret = gidsplit(args.gidsplit, format=args.gid_format) 1919 | elif args.giddump: 1920 | ret = giddump(args, parser) 1921 | elif args.lint: 1922 | ret = lint(args, parser) 1923 | elif args.convert: 1924 | ret = convert(args, parser) 1925 | else: 1926 | parser.error('Something is wrong with the options or the handling of them.') 1927 | 1928 | return ret 1929 | 1930 | 1931 | def main(): 1932 | parser = cmdline_parser() 1933 | args = parser.parse_args() 1934 | run_with_args(args, parser) 1935 | 1936 | if __name__ == '__main__': 1937 | if PY3: 1938 | sys.stdout = codecs.getwriter('utf8')(sys.stdout.buffer) 1939 | sys.stderr = codecs.getwriter('utf8')(sys.stderr.buffer) 1940 | sys.exit(main()) 1941 | --------------------------------------------------------------------------------