Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 1 | #! /usr/bin/env python |
| 2 | # encoding: utf-8 |
| 3 | # XCode 3/XCode 4 generator for Waf |
| 4 | # Nicolas Mercier 2011 |
| 5 | |
| 6 | """ |
| 7 | Usage: |
| 8 | |
| 9 | def options(opt): |
| 10 | opt.load('xcode') |
| 11 | |
| 12 | $ waf configure xcode |
| 13 | """ |
| 14 | |
| 15 | # TODO: support iOS projects |
| 16 | |
| 17 | from waflib import Context, TaskGen, Build, Utils |
Alexander Afanasyev | e334215 | 2016-03-20 11:23:57 -0700 | [diff] [blame] | 18 | import os, sys |
Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 19 | |
| 20 | HEADERS_GLOB = '**/(*.h|*.hpp|*.H|*.inl)' |
| 21 | |
| 22 | MAP_EXT = { |
| 23 | '.h' : "sourcecode.c.h", |
| 24 | |
| 25 | '.hh': "sourcecode.cpp.h", |
| 26 | '.inl': "sourcecode.cpp.h", |
| 27 | '.hpp': "sourcecode.cpp.h", |
| 28 | |
| 29 | '.c': "sourcecode.c.c", |
| 30 | |
| 31 | '.m': "sourcecode.c.objc", |
| 32 | |
| 33 | '.mm': "sourcecode.cpp.objcpp", |
| 34 | |
| 35 | '.cc': "sourcecode.cpp.cpp", |
| 36 | |
| 37 | '.cpp': "sourcecode.cpp.cpp", |
| 38 | '.C': "sourcecode.cpp.cpp", |
| 39 | '.cxx': "sourcecode.cpp.cpp", |
| 40 | '.c++': "sourcecode.cpp.cpp", |
| 41 | |
| 42 | '.l': "sourcecode.lex", # luthor |
| 43 | '.ll': "sourcecode.lex", |
| 44 | |
| 45 | '.y': "sourcecode.yacc", |
| 46 | '.yy': "sourcecode.yacc", |
| 47 | |
| 48 | '.plist': "text.plist.xml", |
| 49 | ".nib": "wrapper.nib", |
| 50 | ".xib": "text.xib", |
| 51 | } |
| 52 | |
| 53 | |
| 54 | part1 = 0 |
| 55 | part2 = 10000 |
| 56 | part3 = 0 |
| 57 | id = 562000999 |
| 58 | def newid(): |
| 59 | global id |
| 60 | id = id + 1 |
| 61 | return "%04X%04X%04X%012d" % (0, 10000, 0, id) |
| 62 | |
| 63 | class XCodeNode: |
| 64 | def __init__(self): |
| 65 | self._id = newid() |
| 66 | |
| 67 | def tostring(self, value): |
| 68 | if isinstance(value, dict): |
| 69 | result = "{\n" |
| 70 | for k,v in value.items(): |
| 71 | result = result + "\t\t\t%s = %s;\n" % (k, self.tostring(v)) |
| 72 | result = result + "\t\t}" |
| 73 | return result |
| 74 | elif isinstance(value, str): |
| 75 | return "\"%s\"" % value |
| 76 | elif isinstance(value, list): |
| 77 | result = "(\n" |
| 78 | for i in value: |
| 79 | result = result + "\t\t\t%s,\n" % self.tostring(i) |
| 80 | result = result + "\t\t)" |
| 81 | return result |
| 82 | elif isinstance(value, XCodeNode): |
| 83 | return value._id |
| 84 | else: |
| 85 | return str(value) |
| 86 | |
| 87 | def write_recursive(self, value, file): |
| 88 | if isinstance(value, dict): |
| 89 | for k,v in value.items(): |
| 90 | self.write_recursive(v, file) |
| 91 | elif isinstance(value, list): |
| 92 | for i in value: |
| 93 | self.write_recursive(i, file) |
| 94 | elif isinstance(value, XCodeNode): |
| 95 | value.write(file) |
| 96 | |
| 97 | def write(self, file): |
| 98 | for attribute,value in self.__dict__.items(): |
| 99 | if attribute[0] != '_': |
| 100 | self.write_recursive(value, file) |
| 101 | |
| 102 | w = file.write |
| 103 | w("\t%s = {\n" % self._id) |
| 104 | w("\t\tisa = %s;\n" % self.__class__.__name__) |
| 105 | for attribute,value in self.__dict__.items(): |
| 106 | if attribute[0] != '_': |
| 107 | w("\t\t%s = %s;\n" % (attribute, self.tostring(value))) |
| 108 | w("\t};\n\n") |
| 109 | |
| 110 | |
| 111 | |
| 112 | # Configurations |
| 113 | class XCBuildConfiguration(XCodeNode): |
| 114 | def __init__(self, name, settings = {}, env=None): |
| 115 | XCodeNode.__init__(self) |
| 116 | self.baseConfigurationReference = "" |
| 117 | self.buildSettings = settings |
| 118 | self.name = name |
| 119 | if env and env.ARCH: |
| 120 | settings['ARCHS'] = " ".join(env.ARCH) |
| 121 | |
| 122 | |
| 123 | class XCConfigurationList(XCodeNode): |
| 124 | def __init__(self, settings): |
| 125 | XCodeNode.__init__(self) |
| 126 | self.buildConfigurations = settings |
| 127 | self.defaultConfigurationIsVisible = 0 |
| 128 | self.defaultConfigurationName = settings and settings[0].name or "" |
| 129 | |
| 130 | # Group/Files |
| 131 | class PBXFileReference(XCodeNode): |
| 132 | def __init__(self, name, path, filetype = '', sourcetree = "SOURCE_ROOT"): |
| 133 | XCodeNode.__init__(self) |
| 134 | self.fileEncoding = 4 |
| 135 | if not filetype: |
| 136 | _, ext = os.path.splitext(name) |
| 137 | filetype = MAP_EXT.get(ext, 'text') |
| 138 | self.lastKnownFileType = filetype |
| 139 | self.name = name |
| 140 | self.path = path |
| 141 | self.sourceTree = sourcetree |
| 142 | |
| 143 | class PBXGroup(XCodeNode): |
| 144 | def __init__(self, name, sourcetree = "<group>"): |
| 145 | XCodeNode.__init__(self) |
| 146 | self.children = [] |
| 147 | self.name = name |
| 148 | self.sourceTree = sourcetree |
| 149 | |
| 150 | def add(self, root, sources): |
| 151 | folders = {} |
| 152 | def folder(n): |
Alexander Afanasyev | b6392e3 | 2014-05-12 23:43:50 -0700 | [diff] [blame] | 153 | if not n.is_child_of(root): |
Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 154 | return self |
| 155 | try: |
| 156 | return folders[n] |
| 157 | except KeyError: |
| 158 | f = PBXGroup(n.name) |
| 159 | p = folder(n.parent) |
| 160 | folders[n] = f |
| 161 | p.children.append(f) |
| 162 | return f |
| 163 | for s in sources: |
| 164 | f = folder(s.parent) |
| 165 | source = PBXFileReference(s.name, s.abspath()) |
| 166 | f.children.append(source) |
| 167 | |
| 168 | |
| 169 | # Targets |
| 170 | class PBXLegacyTarget(XCodeNode): |
| 171 | def __init__(self, action, target=''): |
| 172 | XCodeNode.__init__(self) |
| 173 | self.buildConfigurationList = XCConfigurationList([XCBuildConfiguration('waf', {})]) |
| 174 | if not target: |
| 175 | self.buildArgumentsString = "%s %s" % (sys.argv[0], action) |
| 176 | else: |
Alexander Afanasyev | e334215 | 2016-03-20 11:23:57 -0700 | [diff] [blame] | 177 | self.buildArgumentsString = "%s %s --targets=%s" % (sys.argv[0], action, target) |
Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 178 | self.buildPhases = [] |
| 179 | self.buildToolPath = sys.executable |
| 180 | self.buildWorkingDirectory = "" |
| 181 | self.dependencies = [] |
| 182 | self.name = target or action |
| 183 | self.productName = target or action |
| 184 | self.passBuildSettingsInEnvironment = 0 |
| 185 | |
| 186 | class PBXShellScriptBuildPhase(XCodeNode): |
| 187 | def __init__(self, action, target): |
| 188 | XCodeNode.__init__(self) |
| 189 | self.buildActionMask = 2147483647 |
| 190 | self.files = [] |
| 191 | self.inputPaths = [] |
| 192 | self.outputPaths = [] |
| 193 | self.runOnlyForDeploymentPostProcessing = 0 |
| 194 | self.shellPath = "/bin/sh" |
Alexander Afanasyev | e334215 | 2016-03-20 11:23:57 -0700 | [diff] [blame] | 195 | self.shellScript = "%s %s %s --targets=%s" % (sys.executable, sys.argv[0], action, target) |
Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 196 | |
| 197 | class PBXNativeTarget(XCodeNode): |
| 198 | def __init__(self, action, target, node, env): |
| 199 | XCodeNode.__init__(self) |
| 200 | conf = XCBuildConfiguration('waf', {'PRODUCT_NAME':target, 'CONFIGURATION_BUILD_DIR':node.parent.abspath()}, env) |
| 201 | self.buildConfigurationList = XCConfigurationList([conf]) |
| 202 | self.buildPhases = [PBXShellScriptBuildPhase(action, target)] |
| 203 | self.buildRules = [] |
| 204 | self.dependencies = [] |
| 205 | self.name = target |
| 206 | self.productName = target |
| 207 | self.productType = "com.apple.product-type.application" |
| 208 | self.productReference = PBXFileReference(target, node.abspath(), 'wrapper.application', 'BUILT_PRODUCTS_DIR') |
| 209 | |
| 210 | # Root project object |
| 211 | class PBXProject(XCodeNode): |
| 212 | def __init__(self, name, version): |
| 213 | XCodeNode.__init__(self) |
| 214 | self.buildConfigurationList = XCConfigurationList([XCBuildConfiguration('waf', {})]) |
| 215 | self.compatibilityVersion = version[0] |
Alexander Afanasyev | e334215 | 2016-03-20 11:23:57 -0700 | [diff] [blame] | 216 | self.hasScannedForEncodings = 1 |
Alexander Afanasyev | d1a75b8 | 2013-09-25 16:50:04 -0700 | [diff] [blame] | 217 | self.mainGroup = PBXGroup(name) |
| 218 | self.projectRoot = "" |
| 219 | self.projectDirPath = "" |
| 220 | self.targets = [] |
| 221 | self._objectVersion = version[1] |
| 222 | self._output = PBXGroup('out') |
| 223 | self.mainGroup.children.append(self._output) |
| 224 | |
| 225 | def write(self, file): |
| 226 | w = file.write |
| 227 | w("// !$*UTF8*$!\n") |
| 228 | w("{\n") |
| 229 | w("\tarchiveVersion = 1;\n") |
| 230 | w("\tclasses = {\n") |
| 231 | w("\t};\n") |
| 232 | w("\tobjectVersion = %d;\n" % self._objectVersion) |
| 233 | w("\tobjects = {\n\n") |
| 234 | |
| 235 | XCodeNode.write(self, file) |
| 236 | |
| 237 | w("\t};\n") |
| 238 | w("\trootObject = %s;\n" % self._id) |
| 239 | w("}\n") |
| 240 | |
| 241 | def add_task_gen(self, tg): |
| 242 | if not getattr(tg, 'mac_app', False): |
| 243 | self.targets.append(PBXLegacyTarget('build', tg.name)) |
| 244 | else: |
| 245 | target = PBXNativeTarget('build', tg.name, tg.link_task.outputs[0].change_ext('.app'), tg.env) |
| 246 | self.targets.append(target) |
| 247 | self._output.children.append(target.productReference) |
| 248 | |
| 249 | class xcode(Build.BuildContext): |
| 250 | cmd = 'xcode' |
| 251 | fun = 'build' |
| 252 | |
| 253 | def collect_source(self, tg): |
| 254 | source_files = tg.to_nodes(getattr(tg, 'source', [])) |
| 255 | plist_files = tg.to_nodes(getattr(tg, 'mac_plist', [])) |
| 256 | resource_files = [tg.path.find_node(i) for i in Utils.to_list(getattr(tg, 'mac_resources', []))] |
| 257 | include_dirs = Utils.to_list(getattr(tg, 'includes', [])) + Utils.to_list(getattr(tg, 'export_dirs', [])) |
| 258 | include_files = [] |
| 259 | for x in include_dirs: |
| 260 | if not isinstance(x, str): |
| 261 | include_files.append(x) |
| 262 | continue |
| 263 | d = tg.path.find_node(x) |
| 264 | if d: |
| 265 | lst = [y for y in d.ant_glob(HEADERS_GLOB, flat=False)] |
| 266 | include_files.extend(lst) |
| 267 | |
| 268 | # remove duplicates |
| 269 | source = list(set(source_files + plist_files + resource_files + include_files)) |
| 270 | source.sort(key=lambda x: x.abspath()) |
| 271 | return source |
| 272 | |
| 273 | def execute(self): |
| 274 | """ |
| 275 | Entry point |
| 276 | """ |
| 277 | self.restore() |
| 278 | if not self.all_envs: |
| 279 | self.load_envs() |
| 280 | self.recurse([self.run_dir]) |
| 281 | |
| 282 | appname = getattr(Context.g_module, Context.APPNAME, os.path.basename(self.srcnode.abspath())) |
| 283 | p = PBXProject(appname, ('Xcode 3.2', 46)) |
| 284 | |
| 285 | for g in self.groups: |
| 286 | for tg in g: |
| 287 | if not isinstance(tg, TaskGen.task_gen): |
| 288 | continue |
| 289 | |
| 290 | tg.post() |
| 291 | |
| 292 | features = Utils.to_list(getattr(tg, 'features', '')) |
| 293 | |
| 294 | group = PBXGroup(tg.name) |
| 295 | group.add(tg.path, self.collect_source(tg)) |
| 296 | p.mainGroup.children.append(group) |
| 297 | |
| 298 | if 'cprogram' or 'cxxprogram' in features: |
| 299 | p.add_task_gen(tg) |
| 300 | |
| 301 | |
| 302 | # targets that don't produce the executable but that you might want to run |
| 303 | p.targets.append(PBXLegacyTarget('configure')) |
| 304 | p.targets.append(PBXLegacyTarget('dist')) |
| 305 | p.targets.append(PBXLegacyTarget('install')) |
| 306 | p.targets.append(PBXLegacyTarget('check')) |
| 307 | node = self.srcnode.make_node('%s.xcodeproj' % appname) |
| 308 | node.mkdir() |
| 309 | node = node.make_node('project.pbxproj') |
| 310 | p.write(open(node.abspath(), 'w')) |
Alexander Afanasyev | e334215 | 2016-03-20 11:23:57 -0700 | [diff] [blame] | 311 | |
| 312 | |