Source

SCons / src / engine / SCons / Tool / xcodeproj.py

The default branch has multiple heads

"""SCons.Tool.xcodeproj

Tool-specific initialization for Xcode project files.

There normally shouldn't be any need to import this module directly.
It will usually be imported through the generic SCons.Tool.Tool()
selection method.

"""

#
# __COPYRIGHT__
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"

import os

import SCons.Builder
import SCons.Node.FS

contents_xcworkspacedata = """\
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:%s">
   </FileRef>
</Workspace>
"""

project_pbxproj_header = """\
// !$*UTF8*$!
{
    archiveVersion = 1;
    classes = {
    };
    objectVersion = 46;
    objects = {

"""

project_pbxproj_footer = """\
    };
    rootObject = %s;
}
"""

MAP_EXT = {
    '.h' :  "sourcecode.c.h",

    '.hh':  "sourcecode.cpp.h",
    '.inl': "sourcecode.cpp.h",
    '.hpp': "sourcecode.cpp.h",

    '.c':   "sourcecode.c.c",

    '.m':   "sourcecode.c.objc",

    '.mm':  "sourcecode.cpp.objcpp",

    '.cc':  "sourcecode.cpp.cpp",

    '.cpp': "sourcecode.cpp.cpp",
    '.C':   "sourcecode.cpp.cpp",
    '.cxx': "sourcecode.cpp.cpp",
    '.c++': "sourcecode.cpp.cpp",

    '.l':   "sourcecode.lex", # luthor
    '.ll':  "sourcecode.lex",

    '.y':   "sourcecode.yacc",
    '.yy':  "sourcecode.yacc",

    '.plist': "text.plist.xml",
    ".nib":   "wrapper.nib",
    ".xib":   "text.xib",
    "": "",
}

class XcodeProjectBuildContext(object):
    def __init__(self):
        self.id = 562000999
        self.nodes = {}
        self.generate_codenav_targets = True

    def newid(self):
        self.id = self.id + 1
        return "%04X%04X%04X%012d" % (0, 10000, 0, self.id)

    def regNode(self, node):
        type_name = node.__class__.__name__

        if not self.nodes.has_key(type_name):
            self.nodes[type_name] = []

        self.nodes[type_name].append(node)

    def print_section(self, file, t):
        if self.nodes.has_key(t.__name__):
            file.write('/* Begin %s section */\n' % t.__name__)
            for n in self.nodes[t.__name__]:
                n.write(file)
            file.write('/* End %s section */\n\n' % t.__name__)

class XcodeNode(object):
    def __init__(self, context, description):
        context.regNode(self)

        self._context = context
        self._id = context.newid()
        self._name = description
        
        self.isa = self.__class__.__name__

    def tostring(self, value):
        if isinstance(value, list):
            result = '(\n'
            for i in value:
                result = result + '\t\t\t\t%s,\n' % self.tostring(i)
            result = result + '\t\t\t)'
            return result
        elif isinstance(value, dict):
            result = '{\n'
            for k, v in value.iteritems():
                result = result + ('\t\t\t\t%s = %s;\n' % (self.tostring(k), self.tostring(v)))
            result = result + '\t\t\t}'
            return result
        elif isinstance(value, XcodeNode):
            return value._id + ' /* ' + value._name + ' */' if value._name != '' else value._id
        else:
            return str(value)

    def write(self, f):
        if self._name != '':
            f.write('\t\t%s /* %s */ = {\n' % (self._id, self._name))
        else:
            f.write('\t\t%s = {\n' % (self._id))

        for attribute,value in self.__dict__.items():
            if attribute[0] != '_':
                f.write('\t\t\t%s = %s;\n' % (attribute, self.tostring(value)))

        f.write('\t\t};\n')

class PBXFileReference(XcodeNode):
    def __init__(self, context, path, executable = False):
        XcodeNode.__init__(self, context, os.path.basename(path))

        if not executable:
            self.fileEncoding = 4
            self.name = self._name
            self.path = path
            self.sourceTree = '"<group>"'

            _, filetype = os.path.splitext(self._name)
            self.lastKnownFileType = MAP_EXT[filetype]
        else:
            self.explicitFileType = '"compiled.mach-o.executable"'
            self.includeInIndex = 0
            self.path = path
            self.sourceTree = 'BUILT_PRODUCTS_DIR'

    def is_header_file(self):
        _, ext = os.path.splitext(self._name)
        return ext in ['.h', '.hh', '.hpp']

    def is_inline_file(self):
        _, ext = os.path.splitext(self._name)
        return ext in ['.inl']

    def is_source_file(self):
        _, ext = os.path.splitext(self._name)
        return ext in ['.c', '.cpp', '.cc', '.m', '.mm', '.cxx']

class PBXLegacyTarget(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)

        self.buildArgumentsString = '""'#'"${ACTION)"'
        self.buildConfigurationList = XCConfigurationList(context, 'Build configuration list for PBXLegacyTarget "%s"' % description)
        self.buildPhases = []
        self.buildToolPath = 'scons'
        self.dependencies = []
        self.productName = '"%s"' % description
        self.name = '"%s"' % description
        self.passBuildSettingsInEnvironment = 1
        self.productType = '"com.apple.product-type.tool"'
        #self.productType = '"com.apple.product-type.library.static"'
        #self.productType = '"com.apple.product-type.library.dynamic"'

class PBXNativeTarget(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)

        self.buildConfigurationList = XCConfigurationList(context, 'Build configuration list for PBXNativeTarget "%s"' % description)
        self.buildPhases = [PBXSourcesBuildPhase(context, 'Sources')]
        self.buildRules = []
        self.dependencies = []
        self.name = self._name
        self.productName = self._name
        self.productType = '"com.apple.product-type.tool"'

class PBXProject(XcodeNode):
    def __init__(self, context, description, project_name):
        XcodeNode.__init__(self, context, description)

        self.targets = [PBXLegacyTarget(context, project_name.split('.')[0])]
        if context.generate_codenav_targets:
            self.targets.append(PBXNativeTarget(context, project_name.split('.')[0] + '-codenav'))

        self.buildConfigurationList = XCConfigurationList(context, 'Build configuration list for PBXProject "%s"' % project_name)
        self.compatibilityVersion = '"Xcode 3.2"'
        self.projectDirPath = '""';
        self.projectRoot = '""'
        self.mainGroup = PBXGroup(context, '')

class PBXGroup(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)

        self.sourceTree = '"<group>"'
        self.children = []

        if description != '':
            self.name = description

        self._subgroups = {}

    def add_source(self, source, executable = False):
        l = source.split(os.path.sep)

        currGroup = self
        path = ''
        while len(l) > 1:
            dir_name = l.pop(0)
            path = os.path.join(path, dir_name)

            if not currGroup._subgroups.has_key(dir_name):
                newGroup = PBXGroup(self._context, dir_name)
                currGroup._subgroups[dir_name] = newGroup
                currGroup.children.append(newGroup)
            
            currGroup = currGroup._subgroups[dir_name]

        p = PBXFileReference(self._context, source, executable)
        currGroup.children.append(p)
        return p

class PBXSourcesBuildPhase(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)
        self.buildActionMask = 2147483647
        self.runOnlyForDeploymentPostprocessing = 0
        self.files = []

class PBXBuildFile(XcodeNode):
    def __init__(self, context, ref):
        XcodeNode.__init__(self, context, ref._name)
        self.fileRef = ref

class XCBuildConfiguration(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)

        self.buildSettings = {
            'BUILT_PRODUCTS_DIR': '"/Users/stopiccot/code/other/scons-xcode"', 
            'ARCHS': '"$(ARCHS_STANDARD_64_BIT)"',
            'HEADER_SEARCH_PATHS': '/Users/stopiccot/code/other/scons-xcode/uber_lib',
            'USER_HEADER_SEARCH_PATHS': '/Users/stopiccot/code/other/scons-xcode/uber_lib',
            'PRODUCT_NAME': '"$(TARGET_NAME)"'
        }

        self.name = self._name

class XCConfigurationList(XcodeNode):
    def __init__(self, context, description):
        XcodeNode.__init__(self, context, description)

        self.buildConfigurations = [XCBuildConfiguration(context, 'Debug'), XCBuildConfiguration(context, 'Release')]
        self.defaultConfigurationIsVisible = 0
        self.defaultConfigurationName = 'Release'

def XcodeProjectAction(target, source, env):
    project_name = str(target[0].srcnode())
    os.mkdir(project_name)

    # Generate xxx.xcodeproj/project/project.xcworkspace/contents.xcworkspacedata
    os.mkdir(os.path.join(project_name, 'project.xcworkspace'))
    fw = open(os.path.join(project_name, 'project.xcworkspace', 'contents.xcworkspacedata'), 'w')
    fw.write(contents_xcworkspacedata % project_name)

    fp = open(os.path.join(project_name, 'project.pbxproj'), 'w')

    fp.write(project_pbxproj_header)

    def custom_sort(x):
        if isinstance(x, PBXGroup):
            return '0' + x._name
        elif isinstance(x, PBXFileReference):
            if x.is_header_file(): return '1' + x._name
            if x.is_inline_file(): return '2' + x._name
            if x.is_source_file(): return '3' + x._name
            return '4' + x._name
        else:
            return '5' + x._name

    context = XcodeProjectBuildContext()
    context.generate_codenav_targets = env.get('generate_codenav_targets', 1)
    p = PBXProject(context, 'Project object', project_name)
    
    for i in env['srcs']:
        p.mainGroup.add_source(i)

    for i in env['incs']:
        p.mainGroup.add_source(i)

    for g in context.nodes['PBXGroup']:
        g.children.sort(key = custom_sort)

    if context.generate_codenav_targets:
        for f in context.nodes['PBXFileReference']:
            if f.is_source_file():
                p.targets[1].buildPhases[0].files.append(PBXBuildFile(context, f))

    pp = PBXFileReference(context, 'scons-xcode', True)
    p.targets[0].productReference = pp
    p.targets[1].productReference = pp

    context.print_section(fp, PBXFileReference)
    context.print_section(fp, PBXGroup)
    context.print_section(fp, PBXSourcesBuildPhase)
    context.print_section(fp, PBXLegacyTarget)
    context.print_section(fp, PBXNativeTarget)
    context.print_section(fp, PBXProject)
    context.print_section(fp, PBXBuildFile)
    context.print_section(fp, XCBuildConfiguration)
    context.print_section(fp, XCConfigurationList)

    fp.write(project_pbxproj_footer % p.tostring(p))

def XcodeProjectEmitter(target, source, env):
    project_name = str(target[0].srcnode())
    target.append(os.path.join(project_name, 'project.pbxproj'))

    if env.get('generate_workspace', 1):
        target.append(os.path.join(project_name, 'project.xcworkspace'))
        target.append(os.path.join(project_name, 'project.xcworkspace', 'contents.xcworkspacedata'))

    return target, source

projectBuilder = SCons.Builder.Builder(
#    emitter = XcodeProjectEmitter,
    action = XcodeProjectAction,
    suffix = '.xcodeproj',
    target_factory = SCons.Node.FS.Dir
)

def generate(env):
    """Add Builders and construction variables for Xcode project files to an Environment."""

    try:
        env['BUILDERS']['XcodeProject']
    except KeyError:
        env['BUILDERS']['XcodeProject'] = projectBuilder
    
def exists(env):
    return True

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.