mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-05 02:13:40 +00:00
XcodeCapp 4.0 is a major release of our beloved tool. In a nutshell it: - allows to manage multiple projects simultaneously - allows to follow operations and cancel them - has a per project Error and Warning reporting - supports capp_env as you can define additional paths and objj include path per project - much more things
260 lines
9.6 KiB
Python
Executable File
260 lines
9.6 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
||
import os.path
|
||
import re
|
||
import sys
|
||
from mod_pbxproj import XcodeProject
|
||
|
||
|
||
class PBXModifier (object):
|
||
|
||
XCODE_SUPPORT_FOLDER = u".XcodeSupport"
|
||
SLASH_REPLACEMENT = u"::"
|
||
FRAMEWORKS_RE = re.compile(ur"^(.+/Frameworks/(?:Debug|Source)/([^/]+))/.+$")
|
||
XCC_GENERAL_INCLUDE = u"xcc_general_include.h"
|
||
|
||
def __init__(self, projectRootPath=None):
|
||
self.projectRootPath = projectRootPath
|
||
projectName = os.path.basename(projectRootPath)
|
||
self.pbxPath = os.path.join(projectRootPath, projectName + u".xcodeproj", u"project.pbxproj")
|
||
self.project = XcodeProject.Load(self.pbxPath)
|
||
|
||
self._shadowGroup = None
|
||
self._sourceGroup = None
|
||
self._frameworksGroup = None
|
||
|
||
@property
|
||
def frameworksGroup(self):
|
||
if self._frameworksGroup is None:
|
||
self._frameworksGroup = self.project.get_or_create_group(u"Frameworks", parent=self.sourceGroup)
|
||
|
||
return self._frameworksGroup
|
||
|
||
@property
|
||
def shadowGroup(self):
|
||
if self._shadowGroup is None:
|
||
self._shadowGroup = self.project.get_or_create_group(u"Cocoa Classes")
|
||
|
||
return self._shadowGroup
|
||
|
||
@property
|
||
def sourceGroup(self):
|
||
if self._sourceGroup is None:
|
||
self._sourceGroup = self.project.get_or_create_group(u"Cappuccino Source")
|
||
|
||
return self._sourceGroup
|
||
|
||
def update_general_include(self):
|
||
xcc_general_include_path = os.path.join(self.projectRootPath, self.XCODE_SUPPORT_FOLDER, self.XCC_GENERAL_INCLUDE)
|
||
content = u""
|
||
|
||
for path in os.listdir(os.path.join(self.projectRootPath, self.XCODE_SUPPORT_FOLDER)):
|
||
filename = unicode(os.path.basename(path))
|
||
|
||
if filename.endswith(".h") and filename != self.XCC_GENERAL_INCLUDE:
|
||
content += u'#include "{0}"\n'.format(filename)
|
||
|
||
f = open(xcc_general_include_path, "w")
|
||
f.write(content.encode("utf-8"))
|
||
f.close()
|
||
|
||
if len(self.project.get_files_by_os_path(os.path.join(self.XCODE_SUPPORT_FOLDER, self.XCC_GENERAL_INCLUDE))) == 0:
|
||
self.project.add_file(xcc_general_include_path, parent=self.shadowGroup)
|
||
|
||
def file_with_path(self, projectSourcePath):
|
||
relativePath = os.path.relpath(projectSourcePath, self.projectRootPath)
|
||
|
||
for fileRef in [f for f in self.project.objects.values() if f.get("isa") == "PBXFileReference"]:
|
||
filePath = projectSourcePath if fileRef.get("sourceTree") == "<absolute>" else relativePath
|
||
|
||
if fileRef.get("path") == filePath:
|
||
return fileRef
|
||
|
||
return None
|
||
|
||
def group_with_name(self, name):
|
||
groups = self.project.get_groups_by_name(name)
|
||
|
||
return groups[0] if len(groups) == 1 else None
|
||
|
||
def add_file(self, projectSourcePath, shadowHeaderPath, shadowImplementationPath):
|
||
resolvedPath = os.path.realpath(projectSourcePath)
|
||
|
||
# Shadow files are always project-relative
|
||
if not self.file_with_path(shadowHeaderPath):
|
||
self.project.add_file(shadowHeaderPath, parent=self.shadowGroup, tree="SOURCE_ROOT", create_build_files=False)
|
||
|
||
if not self.file_with_path(shadowImplementationPath):
|
||
self.project.add_file(shadowImplementationPath, parent=self.shadowGroup, tree="SOURCE_ROOT", create_build_files=False)
|
||
|
||
# If the file is within the project directory, the file reference will be project-relative, otherwise absolute
|
||
if resolvedPath.startswith(self.projectRootPath):
|
||
tree = "SOURCE_ROOT"
|
||
else:
|
||
tree = "<absolute>"
|
||
|
||
if not self.file_with_path(resolvedPath):
|
||
relativePath = os.path.relpath(projectSourcePath, self.projectRootPath)
|
||
|
||
if relativePath.startswith(u"Frameworks/"):
|
||
parent = self.frameworksGroup
|
||
else:
|
||
parent = self.sourceGroup
|
||
|
||
self.project.add_file(resolvedPath, parent=parent, tree=tree, create_build_files=False)
|
||
|
||
def remove_file(self, projectSourcePath, shadowHeaderPath, shadowImplementationPath):
|
||
resolvedPath = os.path.realpath(projectSourcePath)
|
||
|
||
for path in (shadowHeaderPath, shadowImplementationPath, resolvedPath):
|
||
fileRef = self.file_with_path(path)
|
||
|
||
if fileRef:
|
||
self.project.remove_file(fileRef)
|
||
|
||
def add_framework_resources(self, framework, resourcesPath):
|
||
files = self.project.get_files_by_os_path(resourcesPath, tree="<absolute>")
|
||
|
||
if not files:
|
||
files = self.project.add_file(resourcesPath, parent=None, tree="<absolute>", create_build_files=False)
|
||
|
||
if files:
|
||
files[0]['name'] = framework + u" Resources"
|
||
|
||
def compare_file_ids(self, id1, id2):
|
||
# A few special cases:
|
||
# - Frameworks group always goes last
|
||
# - XCC_GENERAL_INCLUDE always goes after another file
|
||
# - Frameworks/* file always goes after a non-Frameworks file
|
||
obj1 = self.project.get_obj(id1)
|
||
name1 = obj1.get("name", obj1.get("path"))
|
||
|
||
obj2 = self.project.get_obj(id2)
|
||
name2 = obj2.get("name", obj2.get("path"))
|
||
|
||
# Note: the "∕" in "Frameworks∕" is actually Unicode DIVISION_SLASH, not SOLIDUS (forward slash)
|
||
if name1 == u"Frameworks" and obj1.get("isa") == u"PBXGroup":
|
||
return 1
|
||
elif name2 == u"Frameworks" and obj2.get("isa") == u"PBXGroup":
|
||
return -1
|
||
elif name1 == self.XCC_GENERAL_INCLUDE and obj2.get("isa") == u"PBXFileReference":
|
||
return 1
|
||
elif name2 == self.XCC_GENERAL_INCLUDE and obj1.get("isa") == u"PBXFileReference":
|
||
return -1
|
||
elif name1.startswith(u"Frameworks∕") and not name2.startswith(u"Frameworks∕"):
|
||
return 1
|
||
elif name2.startswith(u"Frameworks∕") and not name1.startswith(u"Frameworks∕"):
|
||
return -1
|
||
|
||
return cmp(name1.lower(), name2.lower())
|
||
|
||
def compare_resource_folder_ids(self, id1, id2):
|
||
folder1 = self.project.get_obj(id1)
|
||
name1 = folder1.get("name", folder1.get("path"))
|
||
|
||
folder2 = self.project.get_obj(id2)
|
||
name2 = folder2.get("name", folder2.get("path"))
|
||
|
||
if name1 == u"Resources":
|
||
return -1
|
||
elif name2 == u"Resources":
|
||
return 1
|
||
|
||
return cmp(name1.lower(), name2.lower())
|
||
|
||
def sort_project(self):
|
||
# Sort the files alphabetically in our groups.
|
||
for group in (self.sourceGroup, self.shadowGroup, self._frameworksGroup):
|
||
if group is None:
|
||
continue
|
||
|
||
group.get("children").data.sort(cmp=self.compare_file_ids)
|
||
|
||
# Move resource folders to the top, Resources at the very top
|
||
root_ids = self.project.root_group.get("children").data
|
||
folder_ids = []
|
||
|
||
for id in root_ids:
|
||
item = self.project.get_obj(id)
|
||
name = item.get("name", item.get("path"))
|
||
|
||
if name.endswith(u"Resources") and item.get("lastKnownFileType") == "folder":
|
||
folder_ids.append(id)
|
||
|
||
folder_ids.sort(cmp=self.compare_resource_folder_ids, reverse=True)
|
||
|
||
for id in folder_ids:
|
||
index = root_ids.index(id)
|
||
del root_ids[index]
|
||
root_ids.insert(0, id)
|
||
|
||
def save_project(self):
|
||
if self.project.modified:
|
||
self.project.backup(backup_name=self.project.pbxproj_path + ".backup")
|
||
self.sort_project()
|
||
self.project.saveFormat3_2()
|
||
|
||
def update_project_with_source(self, action, projectSourcePath):
|
||
relativePath = os.path.relpath(projectSourcePath, self.projectRootPath)
|
||
|
||
shadowBasePath = os.path.join(projectRootPath, self.XCODE_SUPPORT_FOLDER)
|
||
shadowBaseName = os.path.splitext(relativePath)[0].replace(u"/", self.SLASH_REPLACEMENT)
|
||
shadowHeaderPath = os.path.join(shadowBasePath, shadowBaseName + ".h")
|
||
shadowImplementationPath = os.path.join(shadowBasePath, shadowBaseName + ".m")
|
||
|
||
if action == "add":
|
||
fileRef = self.file_with_path(shadowHeaderPath)
|
||
|
||
if not fileRef:
|
||
self.update_general_include()
|
||
self.add_file(projectSourcePath, shadowHeaderPath, shadowImplementationPath)
|
||
|
||
match = self.FRAMEWORKS_RE.match(projectSourcePath)
|
||
|
||
if match:
|
||
framework = match.group(2)
|
||
resourcesPath = os.path.realpath(os.path.join(match.group(1), u"Resources"))
|
||
|
||
if os.path.isdir(resourcesPath):
|
||
self.add_framework_resources(framework, resourcesPath)
|
||
|
||
elif action == "remove":
|
||
self.update_general_include()
|
||
self.remove_file(projectSourcePath, shadowHeaderPath, shadowImplementationPath)
|
||
|
||
|
||
#
|
||
# Possible ways to call this script:
|
||
#
|
||
# "add" projectRootPath file
|
||
# "remove" projectRootPath file
|
||
# "update" projectRootPath action file... [action file...]
|
||
#
|
||
# File paths are full project paths, no resolved symlinks.
|
||
# action is "add" or "remove".
|
||
#
|
||
if __name__ == "__main__":
|
||
|
||
action = sys.argv[1]
|
||
projectRootPath = unicode(sys.argv[2])
|
||
modifier = PBXModifier(projectRootPath)
|
||
|
||
if action in ("add", "remove"):
|
||
projectSourcePath = unicode(sys.argv[3])
|
||
modifier.update_project_with_source(action, projectSourcePath)
|
||
|
||
elif action == "update":
|
||
# When the action is "update", it is followed by "add" or "remove", followed by 1+ paths
|
||
args = sys.argv[3:]
|
||
action = unicode(args.pop(0))
|
||
|
||
while len(args):
|
||
arg = unicode(args.pop(0))
|
||
|
||
if arg in ("add", "remove"):
|
||
action = arg
|
||
continue
|
||
else:
|
||
modifier.update_project_with_source(action, arg)
|
||
|
||
modifier.save_project()
|