JumboWu 主页

版权声明:本文为Jumbo原创文章,采用[知识共享 署名-非商业性使用-禁止演绎 4.0 国际 许可协议],转载前请保证理解此协议 原文出处:https://www.jianshu.com/p/ce22f6d72b0c

Mac平台下使用Jenkins自动化构建Unity项目出包(下)

前面两篇文章基本涵盖了大多数内容,不局限于Unity项目,在其他任何项目中也适合,具体项目需求,具体分享哈。

这篇文章,主要涵盖如下: 1、Shell基本命令编写 2、Python编写 3、修改Android项目 4、打包Android项目出包apk 5、修改iOS项目 6、打包iOS项目出包ipa

下面一步一步来介绍:

一、Shell基本命令编写

build.shell $1 $2 $3 $4 $5

#!/bin/bash
declare svnRevision=0
declare buildMode="distribution"
declare Channel="none"
declare versionCode="none"
declare versionName="none"

#获取传入的参数(jenkins传过来的)
if [ $# -gt 0 ]; then
	buildMode=$1
fi
if [ $# -gt 1 ]; then
	svnRevision=$2
fi
if [ $# -gt 2 ]; then
    Channel=$3
fi
if [ $# -gt 3 ]; then
    versionCode=$4
fi
if [ $# -gt 4 ]; then
    versionName=$5
fi

python $(dirname $0)"/BuildScript.py" $(dirname $0)"/../" "UnityProject" $buildMode $svnRevision $Chanel $versionCode $versionName
exit 0

二、Python编写

BuildScripty.py $1 $2 $3 $4 $5

import sys
import os
import time
import shutil

if len(sys.argv) <= 5:
    exit(1)
else:
    WorkSpacePath = sys.argv[1]
    ProjPath = sys.argv[2]
    BuildMode = sys.argv[3]
    UnityPath = sys.argv[4]        
    Channel = sys.argv[5]
    versionCode = sys.argv[6]
    versionName = sys.argv[7]

CleanBuild = 1
if (len(sys.argv) > 8 ):
    CleanBuild = 0

print "unity path is [" + UnityPath + "]"

#编译步骤
1、BuildUnityProject
2、MofiyMacro  #修改Unity中smcs.rsp gmcs.rsp中定义的宏
3、Unity导出Android工程 #
    unityPath = UnityPath
    print "BuildUnityProject::unitypath [" + UnityPath + "]"

    if unityPath == "none":
        unityPath = "/Applications/Unity/Unity.app"
     
    print "BuildUnityProject::unitypath FINAL [" + UnityPath + "]"
    
    method = "AutoBuild.BuildAndroidPlayer"
    if compileScript == "True":        
        method = "AutoBuild.CompileAndroid"
        
    targetPath = absWorkSpace + "/BuildAndroid/"
    if sys.platform == "win32":
        unityCmd = "\"" + unityPath + "\Unity.exe\" -batchmode -projectPath " + absProjectPath + " -executeMethod " + method + " -logFile " + absWorkSpace + "/BuildAndroid/UnityLog.txt" + " -TargetPath=" + targetPath + appName + "Build"
    else:
	    unityCmd = unityPath + "/Contents/MacOS/Unity -batchmode -projectPath " + absProjectPath + " -executeMethod " + method + " -logFile " + " -TargetPath=" + targetPath + appName + "Build"
	

    unityCmd = unityCmd + " -ForceExitEditor"

    if buildMode == "debug":
        unityCmd = unityCmd + " -NoStrip -Development"

    print unityCmd
4、BuildVersionNumModify# Unity项目中定义的Version版本格式1.x.x.svn,修改写入
5、ModifyAndroidProject# 将项目中写好的Android Project项目拷贝到Unity导出的Android项目中,Android项目合并。如果渠道Channel指定了非none目录,拷贝Channel下的目录
6、VersionModify# 获取上面的Version,修改AndroidManifest.xml中的versionCode,versionName

    versionFile = androidProjPath + "/AndroidManifest.xml"
	file = open(versionFile,'r')
	fileStr = file.read()
	file.close()

	versionCodSearch = re.search(r'android:versionCode="(.*?)"',fileStr)
	if versionCodSearch:
            versionCodStr1 = versionCodSearch.group(0)
            versionCodRealStr1 = versionCodSearch.group(1)
            versionCodStr1 = re.sub(versionCodRealStr1,versionCod,versionCodStr1)
            fileStr = re.sub(versionCodSearch.group(0),versionCodStr1,fileStr)
	
	versionSearch = re.search(r'android:versionName="(.*?)"',fileStr)
	if versionSearch:
            versionStr1 = versionSearch.group(0)
            versionRealStr1 = versionSearch.group(1)
            versionStr1 = re.sub(versionRealStr1,versionStr,versionStr1)
            fileStr = re.sub(versionSearch.group(0),versionStr1,fileStr)

	file = open(versionFile,'w')
	file.write(fileStr)
	file.close()

7、BuildAndroidProject

ant命令:
ant clean
ant debug
ant release

切换到Android项目目录
release版本:ant release
debug版本: ant debug


三、修改Android项目

a、Android项目结构

Android Project

b、参考步骤二中的方案4、5、6

四、打包Android项目出包apk

​ ant打包 ​ 参考步骤二中的方案7

五、修改iOS项目

参考步骤二

1、BuildUnityProject
2、ModifyMacro
3、BuildVersionNumModify
4、ModifyXcodeProject
     from mod_pbxproj import XcodeProject #修改XCode工程 【mod_pbxproj.py 文末源码】

   projectPath =  "****/project.pbxproj"
   absProjectPath = os.path.abspath(projectPath)
   project = XcodeProject.Load(absProjectPath)
   
    project.add_framework_search_paths()#添加framework搜寻目录
    project.add_library_search_paths()#添加library搜寻目录
    project.add_header_search_paths()#添加头文件搜寻目录
    project.add_folder()#添加目录自动创建Group
    project.add_file()#添加文件,如:.framework .dylib 
    project.add_other_ldflags('-ObjC')# 添加标志
    project.remove_other_ldflags('-all_load')#移除标志
   
#修改后,备份保存
   if project.modified:
     project.backup()
     project.saveFormat3_2()

5、ModifyPlistFile #info.plist
     import plistlib  #修改info.plist
    plist = plistlib.readPlist(path)
#---------------------------
    #plist 更新 字典类型
#---------------------------
    plist.writePlist(plist, path)


6、CallXCodeBuild
    profile = PROVISION_CER[bundleIdIndex][1]
    sign = PROVISION_CER[bundleIdIndex][2]
  

    cmd = "xcodeBuild clean build -project " + appName + "Build/Unity-iPhone.xcodeproj" + " PROVISIONING_PROFILE=\"" + profile + "\" CODE_SIGN_IDENTITY=\"" + sign +"\" " + "ENABLE_BITCODE=NO"

    if buildMode == "debug" or buildMode == "release":
        cmd = cmd + " -configuration Debug"
    else:
        cmd = cmd + " -configuration Release"
    
    print cmd
    
    if os.system(cmd) != 0:
        exit(10)
    
    os.chdir(path)

六、打包iOS项目出包ipa

   oname = "Release-iphoneos"
    if buildMode == "debug" or buildMode == "release":
        oname = "Debug-iphoneos"

    makeIpaCmd = "xcrun -sdk iphoneos PackageApplication -v " + absWorkSpace + "/BuildiOS/" + appName + "Build/build/" + oname + "/my.app -o " + absWorkSpace + "/result_ios/tmp.ipa"
    os.system(makeIpaCmd)

    #generate dsym file.if the link flag contains -S, the file will be useless
    dsymPath = absWorkSpace + "/BuildiOS/" + appName + "Build/build/my.app.dSYM"
    print "dsymPath: " + dsymPath

    if os.path.exists(dsymPath):
        path = os.getcwd()
        os.chdir(absWorkSpace + "/BuildiOS/" + appName + "Build/build/")
        bashCmd = "zip -r " + absWorkSpace + "/result_ios/tmpDsym.zip my.app.dSYM"
        os.system(bashCmd)
     
    #make a zip file for appstore version
    if buildMode == "distribution":
        path = os.getcwd()
        os.chdir(absWorkSpace + "/BuildiOS/" + appName + "Build/build/")
        bashCmd = "zip -r " + absWorkSpace + "/result_ios/tmp.zip my.app"
        os.system(bashCmd)
      

引用mod_pbxproj.py 修改XCode工程工具

#  Copyright 2012 Calvin Rien
#
#        Licensed under the Apache License, Version 2.0 (the "License");
#        you may not use this file except in compliance with the License.
#        You may obtain a copy of the License at
#
#          http://www.apache.org/licenses/LICENSE-2.0
#
#        Unless required by applicable law or agreed to in writing, software
#        distributed under the License is distributed on an "AS IS" BASIS,
#        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#        See the License for the specific language governing permissions and
#        limitations under the License.

#  A pbxproj file is an OpenStep format plist
#  {} represents dictionary of key=value pairs delimited by ;
#  () represents list of values delimited by ,
#  file starts with a comment specifying the character type
#  // !$*UTF8*$!

#  when adding a file to a project, create the PBXFileReference
#  add the PBXFileReference's guid to a group
#  create a PBXBuildFile with the PBXFileReference's guid
#  add the PBXBuildFile to the appropriate build phase

#  when adding a header search path add
#  HEADER_SEARCH_PATHS = "path/**";
#  to each XCBuildConfiguration object

#  Xcode4 will read either a OpenStep or XML plist.
#  this script uses `plutil` to validate, read and write
#  the pbxproj file.  Plutil is available in OS X 10.2 and higher
#  Plutil can't write OpenStep plists, so I save as XML

import re, uuid, sys, os, shutil, subprocess, datetime, json, types, re, ntpath

from UserDict import IterableUserDict
from UserList import UserList
from xml.dom.minidom import *

regex = '[a-zA-Z0-9\\._/-]*'

class PBXEncoder(json.JSONEncoder):

        def default(self, obj):
                """Tests the input object, obj, to encode as JSON."""

                if isinstance(obj, (PBXList, PBXDict)):
                        return obj.data

                return json.JSONEncoder.default(self, obj)


class PBXDict(IterableUserDict):
        def __init__(self, d=None):
                if d:
                        d = dict([(PBXType.Convert(k),PBXType.Convert(v)) for k,v in d.items()])

                IterableUserDict.__init__(self, d)

        def __setitem__(self, key, value):
                IterableUserDict.__setitem__(self, PBXType.Convert(key), PBXType.Convert(value))

        def remove(self, key):
                self.data.pop(PBXType.Convert(key), None)



class PBXList(UserList):
        def __init__(self, l=None):
                if isinstance(l, basestring):
                        UserList.__init__(self)
                        self.add(l)
                        return
                elif l:
                        l = [PBXType.Convert(v) for v in l]

                UserList.__init__(self, l)

        def add(self, value):
                value = PBXType.Convert(value)

                if value in self.data:
                        return False

                self.data.append(value)
                return True

        def remove(self, value):
                value = PBXType.Convert(value)
                if value in self.data:
                        self.data.remove(value)
                        return self.data

        def __setitem__(self, key, value):
                UserList.__setitem__(self, PBXType.Convert(key), PBXType.Convert(value))


class PBXType(PBXDict):
        def __init__(self, d=None):
                PBXDict.__init__(self, d)

                if not self.has_key('isa'):
                        self['isa'] = self.__class__.__name__
                self.id = None

        @staticmethod
        def Convert(o):
                if isinstance(o, list):
                        return PBXList(o)
                elif isinstance(o, dict):
                        isa = o.get('isa')

                        if not isa:
                                return PBXDict(o)

                        cls = globals().get(isa)

                        if cls and issubclass(cls, PBXType):
                                return cls(o)

                        print 'warning: unknown PBX type: %s' % isa
                        return PBXDict(o)
                else:
                        return o

        @staticmethod
        def IsGuid(o):
                return re.match('^[A-F0-9]{24}$', str(o))

        @classmethod
        def GenerateId(cls):
                return ''.join(str(uuid.uuid4()).upper().split('-')[1:])

        @classmethod
        def Create(cls, *args, **kwargs):
                return cls(*args, **kwargs)


class PBXFileReference(PBXType):
        def __init__(self, d=None):
                PBXType.__init__(self, d)
                self.build_phase = None

        types = {
                '.a':('archive.ar', 'PBXFrameworksBuildPhase'),
                '.app': ('wrapper.application', None),
                '.s': ('sourcecode.asm', 'PBXSourcesBuildPhase'),
                '.c': ('sourcecode.c.c', 'PBXSourcesBuildPhase'),
                '.cpp': ('sourcecode.cpp.cpp', 'PBXSourcesBuildPhase'),
                '.framework': ('wrapper.framework','PBXFrameworksBuildPhase'),
                '.h': ('sourcecode.c.h', None),
                '.icns': ('image.icns','PBXResourcesBuildPhase'),
                '.m': ('sourcecode.c.objc', 'PBXSourcesBuildPhase'),
                '.mm': ('sourcecode.cpp.objcpp', 'PBXSourcesBuildPhase'),
                '.nib': ('wrapper.nib', 'PBXResourcesBuildPhase'),
                '.plist': ('text.plist.xml', 'PBXResourcesBuildPhase'),
                '.json': ('text.json', 'PBXResourcesBuildPhase'),
                '.png': ('image.png', 'PBXResourcesBuildPhase'),
                '.rtf': ('text.rtf', 'PBXResourcesBuildPhase'),
                '.tiff': ('image.tiff', 'PBXResourcesBuildPhase'),
                '.txt': ('text', 'PBXResourcesBuildPhase'),
                '.xcodeproj': ('wrapper.pb-project', None),
                '.xib': ('file.xib', 'PBXResourcesBuildPhase'),
                '.strings': ('text.plist.strings', 'PBXResourcesBuildPhase'),
                '.bundle': ('wrapper.plug-in', 'PBXResourcesBuildPhase'),
                '.dylib': ('compiled.mach-o.dylib', 'PBXFrameworksBuildPhase'),
                '.tsd': ('file', 'PBXResourcesBuildPhase'),
                '.xml': ('text', 'PBXResourcesBuildPhase')
        }

        trees = [
                '<absolute>',
                '<group>',
                'BUILT_PRODUCTS_DIR',
                'DEVELOPER_DIR',
                'SDKROOT',
                'SOURCE_ROOT',
        ]

        def guess_file_type(self):
                self.remove('explicitFileType')
                self.remove('lastKnownFileType')
                ext = os.path.splitext(self.get('name', ''))[1]

                f_type, build_phase = PBXFileReference.types.get(ext, ('?', None))

                self['lastKnownFileType'] = f_type
                self.build_phase = build_phase

                if f_type == '?':
                        print 'unknown file extension: %s' % ext
                        print 'please add extension and Xcode type to PBXFileReference.types'

                return f_type

        def set_file_type(self, ft):
                self.remove('explicitFileType')
                self.remove('lastKnownFileType')

                self['explicitFileType'] = ft

        @classmethod
        def Create(cls, os_path, tree='SOURCE_ROOT'):
                if tree not in cls.trees:
                        print 'Not a valid sourceTree type: %s' % tree
                        return None

                fr = cls()
                fr.id = cls.GenerateId()
                fr['path'] = os_path
                fr['name'] = os.path.split(os_path)[1]
                fr['sourceTree'] = '<absolute>' if os.path.isabs(os_path) else tree
                fr.guess_file_type()

                return fr

class PBXBuildFile(PBXType):
        def set_weak_link(self, weak=False):
                k_settings = 'settings'
                k_attributes = 'ATTRIBUTES'

                s = self.get(k_settings)

                if not s:
                        if weak:
                                self[k_settings] = PBXDict({k_attributes:PBXList(['Weak'])})

                        return True

                atr = s.get(k_attributes)

                if not atr:
                        if weak:
                                atr = PBXList()
                        else:
                                return False

                if weak:
                        atr.add('Weak')
                else:
                        atr.remove('Weak')

                self[k_settings][k_attributes] = atr

                return True

        def add_compiler_flag(self, flag):
                k_settings = 'settings'
                k_attributes = 'COMPILER_FLAGS'

                if not self.has_key(k_settings):
                        self[k_settings] = PBXDict()

                if not self[k_settings].has_key(k_attributes):
                        self[k_settings][k_attributes] = flag
                        return True

                flags = self[k_settings][k_attributes].split(' ')

                if flag in flags:
                        return False

                flags.append(flag)

                self[k_settings][k_attributes] = ' '.join(flags)

        @classmethod
        def Create(cls, file_ref, weak=False):
                if isinstance(file_ref, PBXFileReference):
                        file_ref = file_ref.id

                bf = cls()
                bf.id = cls.GenerateId()
                bf['fileRef'] = file_ref

                if weak:
                        bf.set_weak_link(True)

                return bf

class PBXGroup(PBXType):
        def add_child(self, ref):
                if not isinstance(ref, PBXDict):
                        return None

                isa = ref.get('isa')

                if isa != 'PBXFileReference' and isa != 'PBXGroup':
                        return None

                if not self.has_key('children'):
                        self['children'] = PBXList()

                self['children'].add(ref.id)

                return ref.id

        def remove_child(self, id):
                if not self.has_key('children'):
                        self['children'] = PBXList()
                        return

                if not PBXType.IsGuid(id):
                        id = id.id

                self['children'].remove(id)

        def has_child(self, id):
                if not self.has_key('children'):
                        self['children'] = PBXList()
                        return False

                if not PBXType.IsGuid(id):
                        id = id.id

                return id in self['children']

        def get_name(self):
                path_name = os.path.split(self.get('path',''))[1]
                return self.get('name', path_name)

        @classmethod
        def Create(cls, name, path=None, tree='SOURCE_ROOT'):
                grp = cls()
                grp.id = cls.GenerateId()
                grp['name'] = name
                grp['children'] = PBXList()

                if path:
                        grp['path'] = path
                        grp['sourceTree'] = tree
                else:
                        grp['sourceTree'] = '<group>'

                return grp


class PBXNativeTarget(PBXType):
        pass


class PBXProject(PBXType):
        pass


class PBXContainerItemProxy(PBXType):
        pass


class PBXReferenceProxy(PBXType):
        pass


class PBXVariantGroup(PBXType):
        pass


class PBXBuildPhase(PBXType):
        def add_build_file(self, bf):
                if bf.get('isa') != 'PBXBuildFile':
                        return False

                if not self.has_key('files'):
                        self['files'] = PBXList()

                self['files'].add(bf.id)

                return True

        def remove_build_file(self, id):
                if not self.has_key('files'):
                        self['files'] = PBXList()
                        return

                self['files'].remove(id)

        def has_build_file(self, id):
                if not self.has_key('files'):
                        self['files'] = PBXList()
                        return False

                if not PBXType.IsGuid(id):
                        id = id.id

                return id in self['files']


class PBXFrameworksBuildPhase(PBXBuildPhase):
        pass


class PBXResourcesBuildPhase(PBXBuildPhase):
        pass


class PBXShellScriptBuildPhase(PBXBuildPhase):
        pass


class PBXSourcesBuildPhase(PBXBuildPhase):
        pass


class PBXCopyFilesBuildPhase(PBXBuildPhase):
        pass


class XCBuildConfiguration(PBXType):
        def add_search_paths(self, paths, base, key, recursive=True, escape=True):
                modified = False

                if not isinstance(paths, list):
                        paths = [paths]

                if not self.has_key(base):
                        self[base] = PBXDict()

                for path in paths:
                        if recursive and not path.endswith('/**'):
                                path = os.path.join(path, '**')

                        if not self[base].has_key(key):
                                self[base][key] = PBXList()
                        elif isinstance(self[base][key], basestring):
                                self[base][key] = PBXList(self[base][key])

                        if escape :
                                if self[base][key].add('\\"%s\\"' % path):#'\\"%s\\"' % path
                                        modified = True
                        else:
                                if self[base][key].add(path):#'\\"%s\\"' % path
                                        modified = True

                return modified

        def add_header_search_paths(self, paths, recursive=True):
                return self.add_search_paths(paths, 'buildSettings', 'HEADER_SEARCH_PATHS', recursive=recursive, escape = False)

        def add_library_search_paths(self, paths, recursive=True):
                return self.add_search_paths(paths, 'buildSettings', 'LIBRARY_SEARCH_PATHS', recursive=recursive, escape = False)

        def add_framework_search_paths(self, paths, recursive=True):
                return self.add_search_paths(paths, 'buildSettings', 'FRAMEWORK_SEARCH_PATHS', recursive=recursive, escape = False)

        def add_other_cflags(self, flags):
                modified = False

                base = 'buildSettings'
                key = 'OTHER_CFLAGS'

                if isinstance(flags, basestring):
                        flags = PBXList(flags)

                if not self.has_key(base):
                        self[base] = PBXDict()

                for flag in flags:

                        if not self[base].has_key(key):
                                self[base][key] = PBXList()
                        elif isinstance(self[base][key], basestring):
                                self[base][key] = PBXList(self[base][key])

                        if self[base][key].add(flag):
                                self[base][key] = [e for e in self[base][key] if e]
                                modified = True

                return modified

        def add_other_ldflags(self, flags):
                modified = False

                base = 'buildSettings'
                key = 'OTHER_LDFLAGS'

                if isinstance(flags, basestring):
                        flags = PBXList(flags)

                if not self.has_key(base):
                        self[base] = PBXDict()

                for flag in flags:

                        if not self[base].has_key(key):
                                self[base][key] = PBXList()
                        elif isinstance(self[base][key], basestring):
                                self[base][key] = PBXList(self[base][key])

                        if self[base][key].add(flag):
                                self[base][key] = [e for e in self[base][key] if e]
                                modified = True

                return modified

        def remove_other_ldflags(self, flags):
            
                modified = False

                base = 'buildSettings'
                key = 'OTHER_LDFLAGS'

                if isinstance(flags, basestring):
                        flags = PBXList(flags)
            
                if self.has_key(base): #there is flags, so we can "remove" something
                        for flag in flags:
                                if not self[base].has_key(key):
                                        return False
                                elif isinstance(self[base][key], basestring):
                                        self[base][key] = PBXList(self[base][key])
                                
                                result = self[base][key].remove(flag)
                                if result:
                                        self[base][key] = [e for e in self[base][key] if e]
                                        modified = True
                return modified

class XCConfigurationList(PBXType):
        pass


class XcodeProject(PBXDict):
        plutil_path = 'plutil'
        special_folders = ['.bundle', '.framework', '.xcodeproj']

        def __init__(self, d=None, path=None):
                if not path:
                        path = os.path.join(os.getcwd(), 'project.pbxproj')

                self.pbxproj_path =os.path.abspath(path)
                self.source_root = os.path.abspath(os.path.join(os.path.split(path)[0], '..'))

                IterableUserDict.__init__(self, d)

                self.data = PBXDict(self.data)
                self.objects = self.get('objects')
                self.modified = False

                root_id = self.get('rootObject')
                if root_id:
                        self.root_object = self.objects[root_id]
                        root_group_id = self.root_object.get('mainGroup')
                        self.root_group = self.objects[root_group_id]
                else:
                        print "error: project has no root object"
                        self.root_object = None
                        self.root_group = None

                for k,v in self.objects.iteritems():
                        v.id = k

        def add_other_cflags(self, flags):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.add_other_cflags(flags):
                                self.modified = True

        def add_other_ldflags(self, flags):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.add_other_ldflags(flags):
                                self.modified = True

        def remove_other_ldflags(self, flags):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.remove_other_ldflags(flags):
                                self.modified = True

        def add_header_search_paths(self, paths, recursive=True):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.add_header_search_paths(paths, recursive):
                                self.modified = True

        def add_framework_search_paths(self, paths, recursive=True):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.add_framework_search_paths(paths, recursive):
                                self.modified = True

        def add_library_search_paths(self, paths, recursive=True):
                build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

                for b in build_configs:
                        if b.add_library_search_paths(paths, recursive):
                                self.modified = True

                # TODO: need to return value if project has been modified

        def get_obj(self, id):
                return self.objects.get(id)

        def get_ids(self):
                return self.objects.keys()

        def get_files_by_os_path(self, os_path, tree='SOURCE_ROOT'):
                files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                                                        and f.get('path') == os_path
                                                                                        and f.get('sourceTree') == tree]

                return files

        def get_files_by_name(self, name, parent=None):
                if parent:
                        files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                                                        and f.get(name) == name
                                                                                        and parent.has_child(f)]
                else:
                        files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                                                        and f.get(name) == name]

                return files

        def get_build_files(self, id):
                files = [f for f in self.objects.values() if f.get('isa') == 'PBXBuildFile'
                                                                                        and f.get('fileRef') == id]

                return files

        def get_groups_by_name(self, name, parent=None):
                if parent:
                        groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                        and g.get_name() == name
                                        and parent.has_child(g)]
                else:
                        groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                        and g.get_name() == name]

                return groups

        def get_or_create_group(self, name, path=None, parent=None):
                if not name:
                        return None

                if not parent:
                        parent = self.root_group
                elif not isinstance(parent, PBXGroup):
                        # assume it's an id
                        parent = self.objects.get(parent, self.root_group)

                groups = self.get_groups_by_name(name)

                for grp in groups:
                        if parent.has_child(grp.id):
                                return grp

                grp = PBXGroup.Create(name, path)
                parent.add_child(grp)

                self.objects[grp.id] = grp

                self.modified = True

                return grp

        def get_groups_by_os_path(self, path):
                path = os.path.abspath(path)

                groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                        and os.path.abspath(g.get('path','/dev/null')) == path]

                return groups

        def get_build_phases(self, phase_name):
                phases = [p for p in self.objects.values() if p.get('isa') == phase_name]

                return phases

        def get_relative_path(self, os_path):
                return os.path.relpath(os_path, self.source_root)

        def verify_files(self, file_list, parent=None):
                # returns list of files not in the current project.
                if not file_list:
                        return []

                if parent:
                        exists_list = [f.get('name') for f in self.objects.values() if f.get('isa') == 'PBXFileReference' and f.get('name') in file_list and parent.has_child(f)]
                else:
                        exists_list = [f.get('name') for f in self.objects.values() if f.get('isa') == 'PBXFileReference' and f.get('name') in file_list]

                return set(file_list).difference(exists_list)

        def add_folder(self, os_path, parent=None, excludes=None, recursive=True, create_build_files=True):
                if not os.path.isdir(os_path):
                        return []

                if not excludes:
                        excludes = []

                results = []

                if not parent:
                        parent = self.root_group
                elif not isinstance(parent, PBXGroup):
                        # assume it's an id
                        parent = self.objects.get(parent, self.root_group)

                path_dict = {os.path.split(os_path)[0]:parent}
                special_list = []

                for (grp_path, subdirs, files) in os.walk(os_path):
                        parent_folder, folder_name = os.path.split(grp_path)
                        parent = path_dict.get(parent_folder, parent)

                        if [sp for sp in special_list if parent_folder.startswith(sp)]:
                                continue

                        if folder_name.startswith('.'):
                                special_list.append(grp_path)
                                continue

                        if os.path.splitext(grp_path)[1] in XcodeProject.special_folders:
                                # if this file has a special extension (bundle or framework mainly) treat it as a file
                                special_list.append(grp_path)

                                new_files = self.verify_files([folder_name], parent=parent)

                                if new_files:
                                        results.extend(self.add_file(grp_path, parent, create_build_files=create_build_files))

                                continue

                        # create group
                        grp = self.get_or_create_group(folder_name, path=self.get_relative_path(grp_path) , parent=parent)
                        path_dict[grp_path] = grp

                        results.append(grp)

                        file_dict = {}

                        for f in files:
                                if f[0] == '.' or [m for m in excludes if re.match(m,f)]:
                                        continue

                                kwds = {
                                        'create_build_files': create_build_files,
                                        'parent': grp,
                                        'name': f
                                }

                                f_path = os.path.join(grp_path, f)

                                file_dict[f_path] = kwds

                        new_files = self.verify_files([n.get('name') for n in file_dict.values()], parent=grp)

                        add_files = [(k,v) for k,v in file_dict.items() if v.get('name') in new_files]

                        for path, kwds in add_files:
                                kwds.pop('name', None)

                                self.add_file(path, **kwds)

                        if not recursive:
                                break

                for r in results:
                        self.objects[r.id] = r

                return results

        def path_leaf(self,path):
                head, tail = ntpath.split(path)
                return tail or ntpath.basename(head)

        def add_file_if_doesnt_exist(self, f_path, parent=None, tree='SOURCE_ROOT', create_build_files=True, weak=False):
                for obj in self.objects.values() :
                        if 'path' in obj :
                                if self.path_leaf(f_path) == self.path_leaf(obj.get('path')):
                                        return []

                return self.add_file(f_path, parent, tree, create_build_files, weak)

        def add_file(self, f_path, parent=None, tree='SOURCE_ROOT', create_build_files=True, weak=False):
                results = []

                abs_path = ''

                if os.path.isabs(f_path):
                        abs_path = f_path

                        if not os.path.exists(f_path):
                                return results
                        elif tree == 'SOURCE_ROOT':
                                f_path = os.path.relpath(f_path, self.source_root)
                        else:
                                tree = '<absolute>'

                if not parent:
                        parent = self.root_group
                elif not isinstance(parent, PBXGroup):
                        # assume it's an id
                        parent = self.objects.get(parent, self.root_group)

                file_ref = PBXFileReference.Create(f_path, tree)
                parent.add_child(file_ref)
                results.append(file_ref)
                # create a build file for the file ref
                if file_ref.build_phase and create_build_files:
                        phases = self.get_build_phases(file_ref.build_phase)

                        for phase in phases:
                                build_file = PBXBuildFile.Create(file_ref, weak=weak)

                                phase.add_build_file(build_file)
                                results.append(build_file)

                        if abs_path and tree == 'SOURCE_ROOT' and os.path.isfile(abs_path)\
                                and file_ref.build_phase == 'PBXFrameworksBuildPhase':
                                library_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0])
                                self.add_library_search_paths([library_path], recursive=False)

                        if abs_path and tree == 'SOURCE_ROOT' and not os.path.isfile(abs_path)\
                                and file_ref.build_phase == 'PBXFrameworksBuildPhase':

                                framework_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0])
                                self.add_framework_search_paths([framework_path,'$(inherited)'], recursive=False)

                for r in results:
                        self.objects[r.id] = r

                if results:
                        self.modified = True

                return results

        def check_and_repair_framework(self, base):
                name = os.path.basename(base);
                if(".framework" in name):
                        basename = name[:-len(".framework")]

                        finalHeaders = os.path.join(base, "Headers");
                        finalCurrent = os.path.join(base, "Versions/Current");
                        finalLib = os.path.join(base, basename);
                        srcHeaders = "Versions/A/Headers";
                        srcCurrent = "A";
                        srcLib = "Versions/A/"+basename;

                        if(not os.path.exists(finalHeaders)):
                                os.symlink(srcHeaders, finalHeaders);
                        if(not os.path.exists(finalCurrent)):
                                os.symlink(srcCurrent, finalCurrent);
                        if(not os.path.exists(finalLib)):
                                os.symlink(srcLib, finalLib);


        def remove_group(self, grp):
                pass

        def remove_file(self, id):
                pass

        def move_file(self, id, dest_grp=None):
                pass

        def apply_patch(self, patch_path, xcode_path):
                if not os.path.isfile(patch_path) or not os.path.isdir(xcode_path):
                        print 'ERROR: couldn\'t apply "%s" to "%s"' % (patch_path, xcode_path)
                        return

                print 'applying "%s" to "%s"' % (patch_path, xcode_path)

                return subprocess.call(['patch', '-p1', '--forward', '--directory=%s'%xcode_path, '--input=%s'%patch_path])

        def apply_mods(self, mod_dict, default_path=None):
                if not default_path:
                        default_path = os.getcwd()

                keys = mod_dict.keys()

                for k in keys:
                        v = mod_dict.pop(k)

                        mod_dict[k.lower()] = v

                parent = mod_dict.pop('group', None)

                if parent:
                        parent = self.get_or_create_group(parent)

                excludes = mod_dict.pop('excludes', [])

                if excludes:
                        excludes = [re.compile(e) for e in excludes]

                compiler_flags = mod_dict.pop('compiler_flags', {})

                for k,v in mod_dict.items():
                        if k == 'patches':
                                for p in v:
                                        if not os.path.isabs(p):
                                                p = os.path.join(default_path, p)

                                        self.apply_patch(p, self.source_root)
                        elif k == 'folders':
                                # get and compile excludes list
                                # do each folder individually
                                for folder in v:
                                        kwds = {}

                                        # if path contains ':' remove it and set recursive to False
                                        if ':' in folder:
                                                args = folder.split(':')
                                                kwds['recursive'] = False
                                                folder = args.pop(0)

                                        if os.path.isabs(folder) and os.path.isdir(folder):
                                                pass
                                        else:
                                                folder = os.path.join(default_path, folder)
                                                if not os.path.isdir(folder):
                                                        continue

                                        if parent:
                                                kwds['parent'] = parent

                                        if excludes:
                                                kwds['excludes'] = excludes

                                        self.add_folder(folder, **kwds)
                        elif k == 'headerpaths' or k == 'librarypaths':
                                paths = []

                                for p in v:
                                        if p.endswith('/**'):
                                                p = os.path.split(p)[0]

                                        if not os.path.isabs(p):
                                                p = os.path.join(default_path, p)

                                        if not os.path.exists(p):
                                                continue

                                        p = self.get_relative_path(p)

                                        paths.append(os.path.join('$(SRCROOT)', p, "**"))

                                if k == 'headerpaths':
                                        self.add_header_search_paths(paths)
                                else:
                                        self.add_library_search_paths(paths)
                        elif k == 'other_cflags':
                                self.add_other_cflags(v)
                        elif k == 'other_ldflags':
                                self.add_other_ldflags(v)
                        elif k == 'libs' or k == 'frameworks' or k == 'files':
                                paths = {}

                                for p in v:
                                        kwds = {}

                                        if ':' in p:
                                                args = p.split(':')
                                                p = args.pop(0)

                                                if 'weak' in args:
                                                        kwds['weak'] = True

                                        file_path = os.path.join(default_path, p)
                                        search_path, file_name = os.path.split(file_path)

                                        if [m for m in excludes if re.match(m,file_name)]:
                                                continue

                                        try:
                                                expr = re.compile(file_name)
                                        except re.error:
                                                expr = None

                                        if expr and os.path.isdir(search_path):
                                                file_list = os.listdir(search_path)

                                                for f in file_list:
                                                        if [m for m in excludes if re.match(m,f)]:
                                                                continue

                                                        if re.search(expr,f):
                                                                kwds['name'] = f
                                                                paths[os.path.join(search_path, f)] = kwds
                                                                p = None

                                        if k == 'libs':
                                                kwds['parent'] = self.get_or_create_group('Libraries', parent=parent)
                                        elif k == 'frameworks':
                                                kwds['parent'] = self.get_or_create_group('Frameworks', parent=parent)

                                        if p:
                                                kwds['name'] = file_name

                                                if k == 'libs':
                                                        p = os.path.join('usr','lib',p)
                                                        kwds['tree'] = 'SDKROOT'
                                                elif k == 'frameworks':
                                                        p = os.path.join('System','Library','Frameworks',p)
                                                        kwds['tree'] = 'SDKROOT'
                                                elif k == 'files' and not os.path.exists(file_path):
                                                        # don't add non-existent files to the project.
                                                        continue

                                                paths[p] = kwds

                                new_files = self.verify_files([n.get('name') for n in paths.values()])

                                add_files = [(k,v) for k,v in paths.items() if v.get('name') in new_files]

                                for path, kwds in add_files:
                                        kwds.pop('name', None)

                                        if not kwds.has_key('parent') and parent:
                                                kwds['parent'] = parent

                                        self.add_file(path, **kwds)

                if compiler_flags:
                        for k,v in compiler_flags.items():
                                filerefs = []

                                for f in v:
                                        filerefs.extend([fr.id for fr in self.objects.values() if fr.get('isa') == 'PBXFileReference'
                                                                                        and fr.get('name') == f])


                                buildfiles = [bf for bf in self.objects.values() if bf.get('isa') == 'PBXBuildFile'
                                                                                and bf.get('fileRef') in filerefs]

                                for bf in buildfiles:
                                        if bf.add_compiler_flag(k):
                                                self.modified = True


        def backup(self, file_name=None):
                if not file_name:
                        file_name = self.pbxproj_path

                backup_name = "%s.%s.backup" % (file_name, datetime.datetime.now().strftime('%d%m%y-%H%M%S'))

                shutil.copy2(file_name, backup_name)

        #old format
        def save(self, file_name=None):
                if not file_name:
                        file_name = self.pbxproj_path

                # JSON serialize the project and convert that json to an xml plist
                p = subprocess.Popen([XcodeProject.plutil_path, '-convert', 'xml1', '-o', file_name, '-'], stdin=subprocess.PIPE)
                p.communicate(PBXEncoder().encode(self.data))

        #save Xcode 3.2 compatible format
        def saveFormat3_2(self, file_name=None):
                if not file_name:
                        file_name = self.pbxproj_path

                #process to get the section's info and names
                objs = self.data.get('objects');
                sections = dict();
                uuids = dict();
                for key in objs:
                        l = list();
                        if objs.get(key).get('isa') in sections:
                                l = sections.get(objs.get(key).get('isa'))
                        l.append(tuple([key, objs.get(key)]))
                        sections[objs.get(key).get('isa')] = l;

                        if('name' in objs.get(key)):
                                uuids[key] = objs.get(key).get('name')
                        elif('path' in objs.get(key)):
                                uuids[key] = objs.get(key).get('path')
                        else:
                                if(objs.get(key).get('isa')=='PBXProject'):
                                        uuids[objs.get(key).get('buildConfigurationList')] = 'Build configuration list for PBXProject "Unity-iPhone"'
                                elif(objs.get(key).get('isa')[0:3] == 'PBX'):
                                        uuids[key] = objs.get(key).get('isa')[3:-10]
                                else:
                                        uuids[key] = 'Build configuration list for PBXNativeTarget "TARGET_NAME"'

                ro = self.data.get('rootObject')
                uuids[ro] = 'Project Object'

                for key in objs:
                        #transitive references (used in the BuildFile section)
                        if('fileRef' in objs.get(key) and objs.get(key).get('fileRef') in uuids):
                                uuids[key] = uuids[objs.get(key).get('fileRef')]
                        #transitive reference to the target name (used in the Native target section)
                        if(objs.get(key).get('isa') == 'PBXNativeTarget'):
                                uuids[objs.get(key).get('buildConfigurationList')] = uuids[objs.get(key).get('buildConfigurationList')].replace('TARGET_NAME',uuids[key])

                self.uuids = uuids;
                self.sections = sections;

                out = open(file_name,'w')
                out.write('// !$*UTF8*$!\n');
                self._printNewXCodeFormat(out, self.data, '', enters=True)
                out.close()

        @classmethod
        def addslashes(cls,s):
                d = {'"':'\\"', "'":"\\'", "\0":"\\\0", "\\":"\\\\"}
                return ''.join(d.get(c, c) for c in s)

        def _printNewXCodeFormat(self, out, root, deep, enters = True):
                if(isinstance(root,IterableUserDict)):
                        out.write('{')
                        if enters:
                                out.write('\n')

                        isa = root.pop('isa','')
                        if(isa!=''): #keep the isa in the first spot
                                if enters:
                                        out.write('\t'+deep)
                                out.write('isa = ');
                                self._printNewXCodeFormat(out,isa,'\t'+deep, enters=enters);
                                out.write(';')
                                if enters:
                                        out.write('\n')
                                else:
                                        out.write(' ');

                        for key in sorted(root.iterkeys()): #keep the same order as Apple.
                                if enters:
                                        out.write('\t'+deep)

                                if re.match(regex,key).group(0) == key:
                                        out.write(key+' = ');
                                else:
                                        out.write('"'+key+'" = ');

                                if key == 'objects':
                                        out.write('{');#open the objects section
                                        if enters:
                                                out.write('\n')
                                        #root.remove('objects') #remove it to avoid problems

                                        sections = [
                                        ('PBXBuildFile',False),
                                        ('PBXCopyFilesBuildPhase',True),
                                        ('PBXFileReference',False),
                                        ('PBXFrameworksBuildPhase',True),
                                        ('PBXGroup',True),
                                        ('PBXNativeTarget',True),
                                        ('PBXProject',True),
                                        ('PBXResourcesBuildPhase',True),
                                        ('PBXShellScriptBuildPhase',True),
                                        ('PBXSourcesBuildPhase',True),
                                        ('XCBuildConfiguration',True),
                                        ('XCConfigurationList',True)]

                                        for section in sections:        #iterate over the sections
                                                if(self.sections.get(section[0]) == None):
                                                        continue;
                                                        
                                                out.write('\n/* Begin %s section */'%section[0]);
                                                self.sections.get(section[0]).sort(cmp=lambda x,y: cmp(x[0],y[0]))
                                                #if(self.sections.get(section[0])=='PBXGroup' and ):    //add the patch to add the missing but existing files.

                                                for pair in self.sections.get(section[0]):
                                                        key = pair[0]
                                                        value = pair[1]
                                                        out.write('\n')
                                                        if enters:
                                                                out.write('\t\t'+deep)
                                                        out.write(key);
                                                        if(key in self.uuids):
                                                                out.write(" /* "+self.uuids[key]+" */");
                                                        out.write(" = ");
                                                        self._printNewXCodeFormat(out,value,'\t\t'+deep,enters=section[1])
                                                        out.write(';')
                                                out.write('\n/* End %s section */\n'%section[0])
                                        out.write(deep+'\t}');#close of the objects section
                                else:
                                        self._printNewXCodeFormat(out,root[key],'\t'+deep,enters=enters)

                                out.write(';')
                                if enters:
                                        out.write('\n')
                                else:
                                        out.write(' ')
                        root['isa']=isa; #restore the isa for further calls

                        if enters:
                                out.write(deep)
                        out.write('}')
                elif isinstance(root,UserList):
                        out.write('(')
                        if enters:
                                out.write('\n')
                        for value in (root):
                                if enters:
                                        out.write('\t'+deep)
                                self._printNewXCodeFormat(out,value,'\t'+deep,enters=enters)
                                out.write(',')
                                if enters:
                                        out.write('\n')
                        if enters:
                                out.write(deep)
                        out.write(')')
                else:
                        if len(root) > 0 and re.match(regex,root).group(0) == root:
                                out.write(root)
                        else:
                                out.write('"'+XcodeProject.addslashes(root)+'"')
                        if(root in self.uuids):
                                out.write(" /* "+self.uuids[root]+" */");

        @classmethod
        def getJSONFromXML(cls, root):
                result = ''
                if(root.nodeName == 'dict'):
                        result += '{'
                        i=0;
                        for child in root.childNodes:
                                if child.nodeType != Node.ELEMENT_NODE:
                                        continue;
                                if(child.nodeName=='key' and i>0):
                                        result +=','
                                result += XcodeProject.getJSONFromXML(child);
                                i = i + 1
                        result += '}'
                elif root.nodeName == 'key':
                        for node in root.childNodes:
                                if node.nodeType == node.TEXT_NODE:
                                        result += '"'+node.data+'":'
                                        break
                elif root.nodeName == 'array':
                        result += '['
                        i=0;
                        for child in root.childNodes:
                                if child.nodeType != Node.ELEMENT_NODE:
                                        continue;

                                if(i>0):
                                        result += ","
                                result += XcodeProject.getJSONFromXML(child);
                                i = i+1;
                        result += ']'
                elif root.nodeName == 'string':
                        data = '""'
                        for node in root.childNodes:
                                if node.nodeType == node.TEXT_NODE:
                                        data = '"'+XcodeProject.addslashes(node.data).replace('\n','\\n')+'"'
                                        break
                        result += data
                return result;

        @classmethod
        def Load(cls, path):
                cls.plutil_path = os.path.join(os.path.split(__file__)[0], 'plutil')

                if not os.path.isfile(XcodeProject.plutil_path):
                        cls.plutil_path = 'plutil'

                if subprocess.call([XcodeProject.plutil_path,'-lint','-s',path]):
                        print 'ERROR: not a valid .pbxproj file'
                        return None

                # load project by converting to JSON and parse
                p = subprocess.Popen([XcodeProject.plutil_path, '-convert', 'xml1', '-o', '-', path], stdout=subprocess.PIPE)
                rawXML = p.communicate()[0]

                xml = parseString(rawXML);
                jsonStr = XcodeProject.getJSONFromXML(xml.getElementsByTagName('dict')[0]);

                tree = json.loads(jsonStr)
                return XcodeProject(tree, path)

Fork me on GitHub