Skip to content

Commit

Permalink
fixes #1:added support to separate unique and sort in CLI
Browse files Browse the repository at this point in the history
also support verbose print switch
  • Loading branch information
临寒 committed Aug 6, 2014
1 parent 08a235f commit e68723b
Showing 1 changed file with 89 additions and 47 deletions.
136 changes: 89 additions & 47 deletions xUnique.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@
"""

from __future__ import unicode_literals
from __future__ import print_function
from subprocess import (check_output as sp_co, check_call as sp_cc)
from os import path, unlink, rename
from hashlib import md5 as hl_md5
import json
from json import loads as json_loads
from urllib import urlretrieve
from fileinput import (input as fi_input, close as fi_close)
from re import compile as re_compile
from sys import (argv as sys_argv, getfilesystemencoding as sys_get_fs_encoding)
from collections import deque
from filecmp import cmp as filecmp_cmp
from optparse import OptionParser


md5_hex = lambda a_str: hl_md5(a_str.encode('utf-8')).hexdigest().upper()


class XUnique(object):
def __init__(self, xcodeproj_path):
def __init__(self, xcodeproj_path, verbose=False):
# check project path
abs_xcodeproj_path = path.abspath(xcodeproj_path)
if not path.exists(abs_xcodeproj_path):
Expand All @@ -48,11 +50,13 @@ def __init__(self, xcodeproj_path):
self.xcodeproj_path = path.split(self.xcode_pbxproj_path)[0]
else:
raise SystemExit("Path must be dir '.xcodeproj' or file 'project.pbxproj'")
self.vprint = print if verbose else lambda *a, **k: None
self.proj_root = path.basename(self.xcodeproj_path) # example MyProject.xpbproj
self.proj_json = self.pbxproj_to_json()
self.nodes = self.proj_json['objects']
self.root_hex = self.proj_json['rootObject']
self.root_node = self.nodes[self.root_hex]
self.main_group_hex = self.root_node['mainGroup']
self.__result = {}
# initialize root content
self.__result.update(
Expand All @@ -66,7 +70,7 @@ def __init__(self, xcodeproj_path):
def pbxproj_to_json(self):
pbproj_to_json_cmd = ['plutil', '-convert', 'json', '-o', '-', self.xcode_pbxproj_path]
json_unicode_str = sp_co(pbproj_to_json_cmd).decode(sys_get_fs_encoding())
return json.loads(json_unicode_str)
return json_loads(json_unicode_str)

def __set_to_result(self, parent_hex, current_hex, current_path_key):
current_node = self.nodes[current_hex]
Expand All @@ -88,8 +92,14 @@ def __set_to_result(self, parent_hex, current_hex, current_path_key):
})

def unique_pbxproj(self):
"""
iterate all nodes in pbxproj file:
""""""
# with open(path.join(self.xcodeproj_path),'w') as result_file:
# json.dump(self.__result,result_file)
self.unique_project()
self.sort_pbxproj()

def unique_project(self):
"""iterate all nodes in pbxproj file:
PBXProject
XCConfigurationList
Expand All @@ -107,20 +117,18 @@ def unique_pbxproj(self):
PBXVariantGroup
"""
self.__unique_project(self.root_hex)
# with open(path.join(self.xcodeproj_path),'w') as result_file:
# json.dump(self.__result,result_file)
self.replace_uuids_with_file()
self.sort_pbxproj()


def replace_uuids_with_file(self):
print 'replace UUIDs and remove unused UUIDs'
self.vprint('replace UUIDs and remove unused UUIDs')
uuid_ptn = re_compile('(?<=\s)[0-9A-F]{24}(?=[\s;])')
for line in fi_input(self.xcode_pbxproj_path, backup='.bak', inplace=1):
# project.pbxproj is an utf-8 encoded file
line = line.decode('utf-8')
uuid_list = uuid_ptn.findall(line)
if not uuid_list:
print line.encode('utf-8'),
print(line.encode('utf-8'), end='')
else:
new_line = line
# remove line with non-existing element
Expand All @@ -130,9 +138,16 @@ def replace_uuids_with_file(self):
else:
for uuid in uuid_list:
new_line = new_line.replace(uuid, self.__result[uuid]['new_key'])
print new_line.encode('utf-8'),
print(new_line.encode('utf-8'), end='')
fi_close()
unlink(self.xcode_pbxproj_path + '.bak')
tmp_path = self.xcode_pbxproj_path + '.bak'
if filecmp_cmp(self.xcode_pbxproj_path, tmp_path, shallow=False):
unlink(self.xcode_pbxproj_path)
rename(tmp_path, self.xcode_pbxproj_path)
print('Ignore uniquify, no changes made to', self.xcode_pbxproj_path)
else:
unlink(tmp_path)
print('Uniquify done')

def sort_pbxproj_pl(self):
"""
Expand All @@ -146,29 +161,30 @@ def sort_pbxproj_pl(self):
"""
sort_script_path = path.join(path.dirname(path.abspath(__file__)), 'sort-Xcode-project-file-mod2.pl')
if not path.exists(sort_script_path):
print 'downloading sort-Xcode-project-file'
self.vprint('downloading sort-Xcode-project-file')
f_path, http_msgs = urlretrieve(
'https://raw.githubusercontent.com/truebit/webkit/master/Tools/Scripts/sort-Xcode-project-file',
filename=sort_script_path)
if int(http_msgs['content-length']) < 1000: # current is 6430
raise SystemExit(
'Cannot download script file from "https://raw.githubusercontent.com/truebit/webkit/master/Tools/Scripts/sort-Xcode-project-file"')
for line in fi_input(sort_script_path, inplace=1, backup='.bak'):
print line.replace('{24}', '{32}'),
print(line.replace('{24}', '{32}'), end='')
fi_close()
unlink(sort_script_path + '.bak')
print 'sort project.xpbproj file'
self.vprint('sort project.xpbproj file')
sp_cc(['perl', sort_script_path, self.xcode_pbxproj_path])

def sort_pbxproj(self):
print 'sort project.xpbproj file'
self.vprint('sort project.xpbproj file')
uuid_chars = len(self.main_group_hex)
lines = []
files_start_ptn = re_compile('^(\s*)files = \(\s*$')
files_key_ptn = re_compile('(?<=[A-F0-9]{32} \/\* ).+?(?= in )')
files_key_ptn = re_compile('(?<=[A-F0-9]{{{}}} \/\* ).+?(?= in )'.format(uuid_chars))
fc_end_ptn = '\);'
files_flag = False
children_start_ptn = re_compile('^(\s*)children = \(\s*$')
children_pbx_key_ptn = re_compile('(?<=[A-F0-9]{32} \/\* ).+?(?= \*\/)')
children_pbx_key_ptn = re_compile('(?<=[A-F0-9]{{{}}} \/\* ).+?(?= \*\/)'.format(uuid_chars))
child_flag = False
pbx_start_ptn = re_compile('^.*Begin (PBXBuildFile|PBXFileReference) section.*$')
pbx_end_ptn = ('^.*End ', ' section.*$')
Expand Down Expand Up @@ -196,15 +212,15 @@ def file_dir_cmp(x, y):
# files search and sort
files_match = files_start_ptn.search(line)
if files_match:
print line,
print(line, end='')
files_flag = True
if isinstance(fc_end_ptn, unicode):
fc_end_ptn = re_compile(files_match.group(1) + fc_end_ptn)
if files_flag:
if fc_end_ptn.search(line):
if lines:
lines.sort(key=lambda file_str: files_key_ptn.search(file_str).group())
print ''.join(lines).encode('utf-8'),
print(''.join(lines).encode('utf-8'), end='')
lines = []
files_flag = False
fc_end_ptn = '\);'
Expand All @@ -213,16 +229,17 @@ def file_dir_cmp(x, y):
# children search and sort
children_match = children_start_ptn.search(line)
if children_match:
print line,
print(line, end='')
child_flag = True
if isinstance(fc_end_ptn, unicode):
fc_end_ptn = re_compile(children_match.group(1) + fc_end_ptn)
if child_flag:
if fc_end_ptn.search(line):
if lines:
if self.__result[self.__main_group_hex]['new_key'] not in last_two[0]:
lines.sort(key=lambda file_str: children_pbx_key_ptn.search(file_str).group(),cmp=file_dir_cmp)
print ''.join(lines).encode('utf-8'),
if self.main_group_hex not in last_two[0]:
lines.sort(key=lambda file_str: children_pbx_key_ptn.search(file_str).group(),
cmp=file_dir_cmp)
print(''.join(lines).encode('utf-8'), end='')
lines = []
child_flag = False
fc_end_ptn = '\);'
Expand All @@ -231,44 +248,44 @@ def file_dir_cmp(x, y):
# PBX search and sort
pbx_match = pbx_start_ptn.search(line)
if pbx_match:
print line,
print(line, end='')
pbx_flag = True
if isinstance(pbx_end_ptn, tuple):
pbx_end_ptn = re_compile(pbx_match.group(1).join(pbx_end_ptn))
if pbx_flag:
if pbx_end_ptn.search(line):
if lines:
lines.sort(key=lambda file_str: children_pbx_key_ptn.search(file_str).group())
print ''.join(lines).encode('utf-8'),
print(''.join(lines).encode('utf-8'), end='')
lines = []
pbx_flag = False
pbx_end_ptn = ('^.*End ', ' section.*')
elif children_pbx_key_ptn.search(line):
lines.append(line)
# normal output
if not (files_flag or child_flag or pbx_flag):
print line,
print(line, end='')
fi_close()
tmp_path = self.xcode_pbxproj_path + '.bak'
if filecmp_cmp(self.xcode_pbxproj_path, tmp_path, shallow=False):
unlink(self.xcode_pbxproj_path)
rename(tmp_path,self.xcode_pbxproj_path)
print 'Ignore, no changes made to {}'.format(self.xcode_pbxproj_path)
rename(tmp_path, self.xcode_pbxproj_path)
print('Ignore sort, no changes made to', self.xcode_pbxproj_path)
else:
unlink(self.xcode_pbxproj_path + '.bak')
unlink(tmp_path)
print('Sort done')

def __unique_project(self, project_hex):
'''PBXProject. It is root itself, no parents to it'''
print 'uniquify PBXProject'
print 'uniquify PBXGroup and PBXFileRef'
self.__main_group_hex = self.root_node['mainGroup']
self.__unique_group_or_ref(project_hex, self.__main_group_hex)
print 'uniquify XCConfigurationList'
self.vprint('uniquify PBXProject')
self.vprint('uniquify PBXGroup and PBXFileRef')
self.__unique_group_or_ref(project_hex, self.main_group_hex)
self.vprint('uniquify XCConfigurationList')
bcl_hex = self.root_node['buildConfigurationList']
self.__unique_build_configuration_list(project_hex, bcl_hex)
subprojects_list = self.root_node.get('projectReferences')
if subprojects_list:
print 'uniquify Subprojects'
self.vprint('uniquify Subprojects')
for subproject_dict in subprojects_list:
product_group_hex = subproject_dict['ProductGroup']
project_ref_parent_hex = subproject_dict['ProjectRef']
Expand All @@ -282,7 +299,7 @@ def __unique_build_configuration_list(self, parent_hex, build_configuration_list
cur_path_key = 'defaultConfigurationName'
self.__set_to_result(parent_hex, build_configuration_list_hex, cur_path_key)
build_configuration_list_node = self.nodes[build_configuration_list_hex]
print 'uniquify XCConfiguration'
self.vprint('uniquify XCConfiguration')
for build_configuration_hex in build_configuration_list_node['buildConfigurations']:
self.__unique_build_configuration(build_configuration_list_hex, build_configuration_hex)

Expand All @@ -293,7 +310,7 @@ def __unique_build_configuration(self, parent_hex, build_configuration_hex):

def __unique_target(self, parent_hex, target_hex):
'''PBXNativeTarget'''
print 'uniquify PBXNativeTarget'
self.vprint('uniquify PBXNativeTarget')
cur_path_key = ('productName', 'name')
self.__set_to_result(parent_hex, target_hex, cur_path_key)
current_node = self.nodes[target_hex]
Expand All @@ -314,7 +331,7 @@ def __unique_target_dependency(self, parent_hex, target_dependency_hex):

def __unique_container_item_proxy(self, parent_hex, container_item_proxy_hex):
'''PBXContainerItemProxy'''
print 'uniquify PBXContainerItemProxy'
self.vprint('uniquify PBXContainerItemProxy')
self.__set_to_result(parent_hex, container_item_proxy_hex, 'remoteInfo')
cur_path = self.__result[container_item_proxy_hex]['path']
current_node = self.nodes[container_item_proxy_hex]
Expand All @@ -333,12 +350,12 @@ def __unique_container_item_proxy(self, parent_hex, container_item_proxy_hex):

def __unique_build_phase(self, parent_hex, build_phase_hex):
'''PBXSourcesBuildPhase PBXFrameworksBuildPhase PBXResourcesBuildPhase PBXCopyFilesBuildPhase'''
print 'uniquify PBXSourcesBuildPhase, PBXFrameworksBuildPhase and PBXResourcesBuildPhase'
self.vprint('uniquify PBXSourcesBuildPhase, PBXFrameworksBuildPhase and PBXResourcesBuildPhase')
current_node = self.nodes[build_phase_hex]
# no useful key, use its isa value
cur_path_key = current_node['isa']
self.__set_to_result(parent_hex, build_phase_hex, cur_path_key)
print 'uniquify PBXBuildFile'
self.vprint('uniquify PBXBuildFile')
for build_file_hex in current_node['files']:
self.__unique_build_file(build_phase_hex, build_file_hex)

Expand Down Expand Up @@ -370,10 +387,35 @@ def __unique_build_file(self, parent_hex, build_file_hex):
self.__result.setdefault('to_be_removed', []).extend((build_file_hex, file_ref_hex))


if __name__ == '__main__':
if len(sys_argv) != 2:
raise SystemExit('usage: xUnique.py path/to/Project.xcodeproj')
else:
xcode_proj_path = sys_argv[1].decode(sys_get_fs_encoding())
xunique = XUnique(xcode_proj_path)
def main(sys_args):
usage = "usage: %prog [[-u|--unique]|[-s|--sort (24|32)]] path/to/Project.xcodeproj"
description = "By default, without any option, xUnique uniquify and sort the project file."
parser = OptionParser(usage=usage, description=description)
parser.add_option("-v", "--verbose",
action="store_true", dest="verbose", default=False,
help="output verbose messages. default is False.")
parser.add_option("-u", "--unique", action="store_true", dest="unique_bool", default=False,
help="uniquify the project file. default is False.")
parser.add_option("-s", "--sort", action="store_true", dest="sort_bool", default=False,
help="sort the project file. default is False.")
(options, args) = parser.parse_args(sys_args[1:])
if len(args) < 1:
parser.print_help()
raise SystemExit("xUnique requires at least one positional argument: relative/absolute path to xcodeproj.")
xcode_proj_path = args[0].decode(sys_get_fs_encoding())
xunique = XUnique(xcode_proj_path, options.verbose)
if not (options.unique_bool or options.sort_bool):
print("Uniquify and Sort")
xunique.unique_pbxproj()
print("Uniquify and Sort done")
else:
if options.unique_bool:
print('Uniquify...')
xunique.unique_project()
if options.sort_bool:
print('Sort...')
xunique.sort_pbxproj()


if __name__ == '__main__':
main(sys_argv)

0 comments on commit e68723b

Please sign in to comment.