New: XcodeCapp 3 total overhaul part 2.

Whew, a complete overhaul of my previous complete overhaul. Changes in no particular order:

- Redesigned the About box.
- Converted tabs to spaces.
- Using Lumberjack for (much better) logging. When debugging, copious logging goes only to Xcode's console, not to the system log. In the release build, only basic info and errors goes to the system log.
- Reformatted the help using Pages, it's a PDF now (so I can use better fonts).
- Massive optimization across the board. All time-consuming operations are done with NSOperation, which uses Grand Central Dispatch, and can be interrupted. So the XCC menu remains fully responsive during source processing.
- Updates to the Xcode project are coalesced so only one read/update/write operation is done.
- Instead of launching a shell and reading the .profile, the target executable is directly launched, but it must reside somewhere in the default binary path + /usr/local/bin:/usr/local/narwhal/bin:~/bin. If jsc, python, objj and nib2cib cannot be found in that path, the user is alerted and XCC quits.
- Added a preference (true by default) to automatically load the Xcode project when a project is opened.
- Added a preference to turn off per-file processing notifications.
- Added a hidden preference to set the log level. Useful for debugging a user's release build.
- Redesigned the Preferences window.
- Renamed some methods/properties, eliminated some unused properties.
- All of the windows remember their position.
- Moved the fsevent_callback into XcodeCapp.m.
- Fixed a bug in mod_pbxproj.py not setting the type of PBXFileReferences which are directories to "folder".
- Added support for categories in parser.j. Woo-hoo!
- Rewrote pbxprojModifier.py to use a class.
- Renamed the Xcode project group names to "Cocoa Classes" and "Cappuccino Source". Within "Cappuccino Source", framework code is kept in a "Frameworks" group.
- Shadow filenames are all project-relative now, thus *much* shorter.
- pbxprojModifier.py keeps the groups and files in the project in sorted order: Resource folders are at the top, followed by Cocoa Classes, followed by Cappuccino Source. Within Cocoa Classes, non-framework files come first, followed by framework files, followed by xcc_general_include.h.
- If either .XcodeSupport or the .xcodeproj is missing, the other is regenerated to ensure they stay in sync.
- A compatibility version for .XcodeSupport is stored inside it in Info.plist. If that version < the app's compatibility version, the project is reset. This ensures that format changes in the future will not result in projects in an unknown state.
- If the Xcode project cannot be opened, the user is alerted and given the option of regenerating the project.
- Resetting the project deletes all .cibs to force the .xibs to be regenerated.
- All possible FSEvents are dealt with individually now, and in a way that (hopefully) maintains sync between Cappuccino and XCC.
- A file descriptor to the Xcode project is kept open so that If the project path changes it can be relocated. If it does move, the user is alerted ad given the option of reloading the project or quitting.
- If one of the watched paths changes, the project is reloaded.
- Xcode creates temporary files, they are properly filtered out now when handling FSEvents.
- NSRegularExpression was being used before, but that is OS X 10.7 only. NSPredicate is used instead now.
-
This commit is contained in:
Aparajita Fishman
2013-05-02 17:04:46 -04:00
parent 6de967345c
commit 550401dffe
39 changed files with 3525 additions and 2772 deletions
@@ -169,20 +169,24 @@ class PBXFileReference(PBXType):
}
trees = [
'<absolute>',
'<group>',
'BUILT_PRODUCTS_DIR',
'DEVELOPER_DIR',
'SDKROOT',
'SOURCE_ROOT',
]
'<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))
if os.path.isdir(self.get('path')):
f_type = 'folder'
build_phase = None
else:
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
@@ -530,7 +534,7 @@ class XcodeProject(PBXDict):
if not path:
path = os.path.join(os.getcwd(), 'project.pbxproj')
self.pbxproj_path =os.path.abspath(path)
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)
@@ -594,7 +598,7 @@ class XcodeProject(PBXDict):
if b.add_library_search_paths(paths, recursive):
self.modified = True
# TODO: need to return value if project has been modified
# TODO: need to return value if project has been modified
def get_obj(self, id):
return self.objects.get(id)
@@ -604,36 +608,36 @@ class XcodeProject(PBXDict):
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]
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)]
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]
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]
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)]
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]
and g.get_name() == name]
return groups
@@ -666,7 +670,7 @@ class XcodeProject(PBXDict):
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]
and os.path.abspath(g.get('path','/dev/null')) == path]
return groups
@@ -816,12 +820,12 @@ class XcodeProject(PBXDict):
results.append(build_file)
if abs_path and tree == 'SOURCE_ROOT' and os.path.isfile(abs_path)\
and file_ref.build_phase == 'PBXFrameworksBuildPhase':
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':
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)
@@ -1039,11 +1043,11 @@ class XcodeProject(PBXDict):
for f in v:
filerefs.extend([fr.id for fr in self.objects.values() if fr.get('isa') == 'PBXFileReference'
and fr.get('name') == f])
and fr.get('name') == f])
buildfiles = [bf for bf in self.objects.values() if bf.get('isa') == 'PBXBuildFile'
and bf.get('fileRef') in filerefs]
and bf.get('fileRef') in filerefs]
for bf in buildfiles:
if bf.add_compiler_flag(k):
@@ -1153,22 +1157,22 @@ class XcodeProject(PBXDict):
#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),
('PBXTargetDependency', True),
('PBXVariantGroup', True),
('PBXReferenceProxy', True),
('PBXContainerItemProxy', True)]
('PBXBuildFile',False),
('PBXCopyFilesBuildPhase',True),
('PBXFileReference',False),
('PBXFrameworksBuildPhase',True),
('PBXGroup',True),
('PBXNativeTarget',True),
('PBXProject',True),
('PBXResourcesBuildPhase',True),
('PBXShellScriptBuildPhase',True),
('PBXSourcesBuildPhase',True),
('XCBuildConfiguration',True),
('XCConfigurationList',True),
('PBXTargetDependency', True),
('PBXVariantGroup', True),
('PBXReferenceProxy', True),
('PBXContainerItemProxy', True)]
for section in sections: #iterate over the sections
if(self.sections.get(section[0]) == None):
@@ -1226,7 +1230,7 @@ class XcodeProject(PBXDict):
out.write('"'+XcodeProject.addslashes(root)+'"')
if(root in self.uuids):
out.write(" /* "+self.uuids[root]+" */");
@classmethod
def getJSONFromXML(cls, root):
result = ''
@@ -1252,7 +1256,7 @@ class XcodeProject(PBXDict):
for child in root.childNodes:
if child.nodeType != Node.ELEMENT_NODE:
continue;
if(i>0):
result += ","
result += XcodeProject.getJSONFromXML(child);
@@ -1266,24 +1270,24 @@ class XcodeProject(PBXDict):
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)
+45 -41
View File
@@ -24,6 +24,7 @@
var FILE = require("file"),
OS = require("os"),
stream = require("narwhal/term").stream,
SLASH_REPLACEMENT = ""; // DIVISION SLASH, Unicode: U+2215
@@ -46,22 +47,12 @@ var errors = [],
{
ClassDeclarationStatement: function(node, st, c)
{
if (node.categoryname)
{
[errors addObject:@{
@"message": "Categories are not supported yet, ignoring it.",
@"path": node.loc.source,
@"line": node.loc.start.line
}];
return;
}
var className = node.classname.name,
superclassname = node.superclassname.name,
superclassname = node.superclassname ? node.superclassname.name : "",
declaredOutletsName = [],
classInfo = {
"name": className,
"category": node.categoryname ? node.categoryname.name : "",
"superClass": superclassname,
"outlets": [],
"actions": [],
@@ -101,28 +92,31 @@ var errors = [],
methodReturnType = [node.returntype ? node.returntype.name : "id"],
methodHasAction = node.action ? "IBAction" : null,
selector = selectors[0].name,
actionInformations = {"name": selector, "arguments":[]};
actionInfo = {"name": selector, "arguments":[]};
if (methodHasAction && arguments.length == 1)
if (methodHasAction)
{
if (st.actionNames.indexOf(selector) !== -1)
raise(node.loc.start, "Action named '" + selector + "' is declared multiple times.");
st.actionNames.push(selector);
for (var i = 0; i < arguments.length; i++)
if (arguments.length == 1)
{
var argument = arguments[i],
argumentName = argument.identifier.name,
argumentType = argument.type ? argument.type.name : null;
if (st.actionNames.indexOf(selector) !== -1)
raise(node.loc.start, "Action named '" + selector + "' is declared multiple times.");
actionInformations.arguments.push({"type": argumentType, "name": argumentName});
st.actionNames.push(selector);
for (var i = 0; i < arguments.length; i++)
{
var argument = arguments[i],
argumentName = argument.identifier.name,
argumentType = argument.type ? argument.type.name : null;
actionInfo.arguments.push({"type": argumentType, "name": argumentName});
}
st.actions.push(actionInfo)
}
st.actions.push(actionInformations)
else
raise(node.loc.start, "Action methods must have exactly one parameter.");
}
else if (methodHasAction)
raise(node.loc.start, "Method '" + selector + "' is an action but has more than one parameter.");
}
}
);
@@ -137,25 +131,32 @@ function compile(node, state, visitor)
c(node, state);
};
function shadowBaseNameForPath(path)
function shadowBaseNameForPath(projectBasePath, path)
{
// Make the path project-relative
path = path.substring(projectBasePath.length + 1, path.length);
// strip the extension and replace slashes
return path.substring(0, path.length - 2).replace(/[/]/g, SLASH_REPLACEMENT);
}
/*
$1 Project base path
$2 Full project source path
*/
function main(args)
{
try
{
var fileURL = new CFURL(args[1]),
outputDirectory = args[2],
baseFilename = shadowBaseNameForPath(fileURL.path()),
var projectBasePath = args[1],
sourcePath = args[2],
outputDirectory = [projectBasePath stringByAppendingPathComponent:@".XcodeSupport"],
baseFilename = shadowBaseNameForPath(projectBasePath, sourcePath),
outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilename + ".h"]),
outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilename + ".m"]),
source = FILE.read(fileURL, { charset: "UTF-8" }),
source = FILE.read(sourcePath, { charset: "UTF-8" }),
flags = ObjectiveJ.Preprocessor.Flags.IncludeDebugSymbols | ObjectiveJ.Preprocessor.Flags.IncludeTypeSignatures,
sourceFile = fileURL.path(),
tokens = ObjectiveJ.acorn.parse(source, { locations:true, sourceFile:sourceFile }),
tokens = ObjectiveJ.acorn.parse(source, { locations:true, sourceFile:sourcePath }),
classesInformation = [],
ObjectiveCSource = "",
ObjectiveCHeader = "",
@@ -166,7 +167,6 @@ function main(args)
// dump(classesInformation)
ObjectiveCHeader +=
"#import <Foundation/Foundation.h>\n" +
"#import <Cocoa/Cocoa.h>\n" +
'#import "xcc_general_include.h"\n';
@@ -176,7 +176,10 @@ function main(args)
classesInformation.forEach(function(aClass)
{
// add new class definition
ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)];
if (aClass.superClass)
ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)];
else
ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", aClass.name, aClass.category];
// add each outlet in header
if (aClass.outlets.length > 0)
@@ -215,10 +218,10 @@ function main(args)
{
[errors addObject:@{
@"message": e.message,
@"path": sourceFile,
@"path": sourcePath,
@"line": e.line
}];
hasErrors = YES;
}
@@ -226,8 +229,8 @@ function main(args)
{
var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0];
print([plist rawString]);
stream.printError([plist rawString]);
// If there were category warnings, hasErrors is NO, so return a warning status
OS.exit(hasErrors ? 1 : 2);
}
@@ -567,6 +570,7 @@ var NSClasses = {
"NSStepper" : YES,
"NSStepperCell" : YES,
"NSString Application Kit Additions" : YES,
"NSTableCellView" : YES,
"NSTableColumn" : YES,
"NSTableHeaderCell" : YES,
"NSTableHeaderView" : YES,
@@ -4,135 +4,254 @@ import re
import sys
from mod_pbxproj import XcodeProject
XCODE_SUPPORT_FOLDER = ".XcodeSupport"
SLASH_REPLACEMENT = u"" # DIVISION SLASH Unicode U+2215
STRING_RE = re.compile(ur"^\s*<string>(.*)</string>\s*$", re.MULTILINE)
FRAMEWORKS_RE = re.compile(ur"^(.+/Frameworks/(?:Debug|Source)/([^/]+))/.+$")
XCC_GENERAL_INCLUDE = u"xcc_general_include.h"
class PBXModifier (object):
def update_general_include(project, projectBasePath, shadowGroup):
xcc_general_include_path = os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER, XCC_GENERAL_INCLUDE)
content = u""
XCODE_SUPPORT_FOLDER = u".XcodeSupport"
SLASH_REPLACEMENT = u"" # DIVISION SLASH Unicode U+2215
STRING_RE = re.compile(ur"^\s*<string>(.*)</string>\s*$", re.MULTILINE)
FRAMEWORKS_RE = re.compile(ur"^(.+/Frameworks/(?:Debug|Source)/([^/]+))/.+$")
XCC_GENERAL_INCLUDE = u"xcc_general_include.h"
for path in os.listdir(os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER)):
filename = unicode(os.path.basename(path))
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)
if filename.endswith(".h") and filename != XCC_GENERAL_INCLUDE:
content += u'#include "{0}"\n'.format(filename)
self.shadowGroup = self.project.get_or_create_group(u"Cocoa Classes")
self.sourceGroup = self.project.get_or_create_group(u"Cappuccino Source")
self._frameworksGroup = None
f = open(xcc_general_include_path, "w")
f.write(content.encode("utf-8"))
f.close()
@property
def frameworksGroup(self):
if self._frameworksGroup is None:
self._frameworksGroup = self.project.get_or_create_group(u"Frameworks", parent=self.sourceGroup)
if len(project.get_files_by_os_path(os.path.join(XCODE_SUPPORT_FOLDER, XCC_GENERAL_INCLUDE))) == 0:
project.add_file(xcc_general_include_path, parent=shadowGroup)
return self._frameworksGroup
def file_with_path(path, projectPath, project):
relPath = os.path.relpath(path, projectPath)
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 fileRef in [f for f in project.objects.values() if f.get("isa") == "PBXFileReference"]:
filePath = path if fileRef.get("sourceTree") == "<absolute>" else relPath
for path in os.listdir(os.path.join(self.projectRootPath, self.XCODE_SUPPORT_FOLDER)):
filename = unicode(os.path.basename(path))
if fileRef.get("path") == filePath:
return fileRef
if filename.endswith(".h") and filename != self.XCC_GENERAL_INCLUDE:
content += u'#include "{0}"\n'.format(filename)
return None
f = open(xcc_general_include_path, "w")
f.write(content.encode("utf-8"))
f.close()
def add_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath):
# Shadow files are always project-relative
if not file_with_path(shadowHeaderPath, projectBasePath, project):
project.add_file(shadowHeaderPath, parent=shadowGroup, tree="SOURCE_ROOT", create_build_files=False)
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)
if not file_with_path(shadowImplementationPath, projectBasePath, project):
project.add_file(shadowImplementationPath, parent=shadowGroup, tree="SOURCE_ROOT", create_build_files=False)
def file_with_path(self, projectSourcePath):
relativePath = os.path.relpath(projectSourcePath, self.projectRootPath)
# If the file is within the project directory, the file reference will be project-relative, otherwise absolute
if sourcePath.startswith(projectBasePath):
tree = "SOURCE_ROOT"
else:
tree = "<absolute>"
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 not file_with_path(sourcePath, projectBasePath, project):
project.add_file(sourcePath, parent=sourceGroup, tree=tree, create_build_files=False)
if fileRef.get("path") == filePath:
return fileRef
def remove_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath):
for path in (shadowHeaderPath, shadowImplementationPath, sourcePath):
fileRef = file_with_path(path, projectBasePath, project)
return None
if fileRef:
project.remove_file(fileRef)
def add_file(self, projectSourcePath, shadowHeaderPath, shadowImplementationPath):
resolvedPath = os.path.realpath(projectSourcePath)
def xml_converter(matchObj):
return "<string>{0}</string>".format(matchObj.group(1).encode('ascii', 'xmlcharrefreplace'))
# 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)
def convert_unicode_to_xml(path):
"""
mod_pbxproj writes unicode data as utf-8, but since it's an xml plist
all non-ascii characters should be converted to xml character references.
So we do that as a post-processing phase.
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 xml_converter(self, matchObj):
return "<string>{0}</string>".format(matchObj.group(1).encode("ascii", "xmlcharrefreplace"))
def convert_unicode_to_xml(self):
"""
with open(path, 'rb') as f:
content = f.read().decode('utf-8')
mod_pbxproj writes unicode data as utf-8, but since it's an xml plist
all non-ascii characters should be converted to xml character references.
So we do that as a post-processing phase.
"""
with open(self.pbxPath, 'rb') as f:
content = f.read().decode('utf-8')
with open(path, "wb") as f:
content = STRING_RE.sub(xml_converter, content)
f.write(content)
with open(self.pbxPath, "wb") as f:
content = self.STRING_RE.sub(self.xml_converter, content)
f.write(content)
def add_framework_resources(project, framework, resourcesPath):
files = project.get_files_by_os_path(resourcesPath, tree="<absolute>")
def add_framework_resources(self, framework, resourcesPath):
files = self.project.get_files_by_os_path(resourcesPath, tree="<absolute>")
if not files:
files = project.add_file(resourcesPath, parent=None, tree="<absolute>", create_build_files=False)
if not files:
files = self.project.add_file(resourcesPath, parent=None, tree="<absolute>", create_build_files=False)
if files:
files[0]['name'] = framework + " Resources"
if files:
files[0]['name'] = framework + u" Resources"
def save_project(project, pbxPath):
project.save()
convert_unicode_to_xml(pbxPath)
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"))
if __name__ == "__main__":
# 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
action = sys.argv[1]
return cmp(name1.lower(), name2.lower())
if action in ("add", "remove"):
projectBasePath = unicode(sys.argv[2])
projectSourcePath = unicode(sys.argv[3])
sourcePath = os.path.realpath(projectSourcePath)
def compare_resource_folder_ids(self, id1, id2):
folder1 = self.project.get_obj(id1)
name1 = folder1.get("name", folder1.get("path"))
shadowBasePath = os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER)
shadowBaseName = os.path.splitext(sourcePath)[0].replace(u"/", SLASH_REPLACEMENT)
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):
self.sort_project()
self.project.save()
self.convert_unicode_to_xml()
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")
projectName = os.path.basename(projectBasePath)
pbxPath = os.path.join(projectBasePath, projectName + ".xcodeproj", "project.pbxproj")
project = XcodeProject.Load(pbxPath)
shadowGroup = project.get_or_create_group("Classes")
sourceGroup = project.get_or_create_group("Sources")
if action == "add":
fileRef = file_with_path(shadowHeaderPath, projectBasePath, project)
fileRef = self.file_with_path(shadowHeaderPath)
if not fileRef:
update_general_include(project, projectBasePath, shadowGroup)
add_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath)
self.update_general_include()
self.add_file(projectSourcePath, shadowHeaderPath, shadowImplementationPath)
match = FRAMEWORKS_RE.match(projectSourcePath)
match = self.FRAMEWORKS_RE.match(projectSourcePath)
if match:
framework = match.group(2)
resourcesPath = os.path.realpath(os.path.join(match.group(1), "Resources"))
resourcesPath = os.path.realpath(os.path.join(match.group(1), u"Resources"))
if os.path.isdir(resourcesPath):
add_framework_resources(project, framework, resourcesPath)
save_project(project, pbxPath)
self.add_framework_resources(framework, resourcesPath)
elif action == "remove":
update_general_include(project, projectBasePath, shadowGroup)
remove_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath)
save_project(project, pbxPath)
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)
modifier.save_project()
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()