mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-27 05:57:04 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user