1#!/usr/bin/env python 2# encoding: utf-8 3# Philipp Bender, 2012 4# Matt Clarkson, 2012 5 6import re, os 7from waflib.Task import Task 8from waflib.TaskGen import extension 9from waflib import Errors, Context 10 11""" 12A simple tool to integrate protocol buffers into your build system. 13 14Example for C++: 15 16 def configure(conf): 17 conf.load('compiler_cxx cxx protoc') 18 19 def build(bld): 20 bld( 21 features = 'cxx cxxprogram' 22 source = 'main.cpp file1.proto proto/file2.proto', 23 includes = '. proto', 24 target = 'executable') 25 26Example for Python: 27 28 def configure(conf): 29 conf.load('python protoc') 30 31 def build(bld): 32 bld( 33 features = 'py' 34 source = 'main.py file1.proto proto/file2.proto', 35 protoc_includes = 'proto') 36 37Example for both Python and C++ at same time: 38 39 def configure(conf): 40 conf.load('cxx python protoc') 41 42 def build(bld): 43 bld( 44 features = 'cxx py' 45 source = 'file1.proto proto/file2.proto', 46 protoc_includes = 'proto') # or includes 47 48 49Example for Java: 50 51 def options(opt): 52 opt.load('java') 53 54 def configure(conf): 55 conf.load('python java protoc') 56 # Here you have to point to your protobuf-java JAR and have it in classpath 57 conf.env.CLASSPATH_PROTOBUF = ['protobuf-java-2.5.0.jar'] 58 59 def build(bld): 60 bld( 61 features = 'javac protoc', 62 name = 'pbjava', 63 srcdir = 'inc/ src', # directories used by javac 64 source = ['inc/message_inc.proto', 'inc/message.proto'], 65 # source is used by protoc for .proto files 66 use = 'PROTOBUF', 67 protoc_includes = ['inc']) # for protoc to search dependencies 68 69 70 71 72Notes when using this tool: 73 74- protoc command line parsing is tricky. 75 76 The generated files can be put in subfolders which depend on 77 the order of the include paths. 78 79 Try to be simple when creating task generators 80 containing protoc stuff. 81 82""" 83 84class protoc(Task): 85 run_str = '${PROTOC} ${PROTOC_FL:PROTOC_FLAGS} ${PROTOC_ST:INCPATHS} ${PROTOC_ST:PROTOC_INCPATHS} ${SRC[0].bldpath()}' 86 color = 'BLUE' 87 ext_out = ['.h', 'pb.cc', '.py', '.java'] 88 def scan(self): 89 """ 90 Scan .proto dependencies 91 """ 92 node = self.inputs[0] 93 94 nodes = [] 95 names = [] 96 seen = [] 97 search_nodes = [] 98 99 if not node: 100 return (nodes, names) 101 102 if 'cxx' in self.generator.features: 103 search_nodes = self.generator.includes_nodes 104 105 if 'py' in self.generator.features or 'javac' in self.generator.features: 106 for incpath in getattr(self.generator, 'protoc_includes', []): 107 search_nodes.append(self.generator.bld.path.find_node(incpath)) 108 109 def parse_node(node): 110 if node in seen: 111 return 112 seen.append(node) 113 code = node.read().splitlines() 114 for line in code: 115 m = re.search(r'^import\s+"(.*)";.*(//)?.*', line) 116 if m: 117 dep = m.groups()[0] 118 for incnode in search_nodes: 119 found = incnode.find_resource(dep) 120 if found: 121 nodes.append(found) 122 parse_node(found) 123 else: 124 names.append(dep) 125 126 parse_node(node) 127 # Add also dependencies path to INCPATHS so protoc will find the included file 128 for deppath in nodes: 129 self.env.append_value('INCPATHS', deppath.parent.bldpath()) 130 return (nodes, names) 131 132@extension('.proto') 133def process_protoc(self, node): 134 incdirs = [] 135 out_nodes = [] 136 protoc_flags = [] 137 138 # ensure PROTOC_FLAGS is a list; a copy is used below anyway 139 self.env.PROTOC_FLAGS = self.to_list(self.env.PROTOC_FLAGS) 140 141 if 'cxx' in self.features: 142 cpp_node = node.change_ext('.pb.cc') 143 hpp_node = node.change_ext('.pb.h') 144 self.source.append(cpp_node) 145 out_nodes.append(cpp_node) 146 out_nodes.append(hpp_node) 147 protoc_flags.append('--cpp_out=%s' % node.parent.get_bld().bldpath()) 148 149 if 'py' in self.features: 150 py_node = node.change_ext('_pb2.py') 151 self.source.append(py_node) 152 out_nodes.append(py_node) 153 protoc_flags.append('--python_out=%s' % node.parent.get_bld().bldpath()) 154 155 if 'javac' in self.features: 156 pkgname, javapkg, javacn, nodename = None, None, None, None 157 messages = [] 158 159 # .java file name is done with some rules depending on .proto file content: 160 # -) package is either derived from option java_package if present 161 # or from package directive 162 # -) file name is either derived from option java_outer_classname if present 163 # or the .proto file is converted to camelcase. If a message 164 # is named the same then the behaviour depends on protoc version 165 # 166 # See also: https://developers.google.com/protocol-buffers/docs/reference/java-generated#invocation 167 168 code = node.read().splitlines() 169 for line in code: 170 m = re.search(r'^package\s+(.*);', line) 171 if m: 172 pkgname = m.groups()[0] 173 m = re.search(r'^option\s+(\S*)\s*=\s*"(\S*)";', line) 174 if m: 175 optname = m.groups()[0] 176 if optname == 'java_package': 177 javapkg = m.groups()[1] 178 elif optname == 'java_outer_classname': 179 javacn = m.groups()[1] 180 if self.env.PROTOC_MAJOR > '2': 181 m = re.search(r'^message\s+(\w*)\s*{*', line) 182 if m: 183 messages.append(m.groups()[0]) 184 185 if javapkg: 186 nodename = javapkg 187 elif pkgname: 188 nodename = pkgname 189 else: 190 raise Errors.WafError('Cannot derive java name from protoc file') 191 192 nodename = nodename.replace('.',os.sep) + os.sep 193 if javacn: 194 nodename += javacn + '.java' 195 else: 196 if self.env.PROTOC_MAJOR > '2' and node.abspath()[node.abspath().rfind(os.sep)+1:node.abspath().rfind('.')].title() in messages: 197 nodename += node.abspath()[node.abspath().rfind(os.sep)+1:node.abspath().rfind('.')].title().replace('_','') + 'OuterClass.java' 198 else: 199 nodename += node.abspath()[node.abspath().rfind(os.sep)+1:node.abspath().rfind('.')].title().replace('_','') + '.java' 200 201 java_node = node.parent.find_or_declare(nodename) 202 out_nodes.append(java_node) 203 protoc_flags.append('--java_out=%s' % node.parent.get_bld().bldpath()) 204 205 # Make javac get also pick java code generated in build 206 if not node.parent.get_bld() in self.javac_task.srcdir: 207 self.javac_task.srcdir.append(node.parent.get_bld()) 208 209 if not out_nodes: 210 raise Errors.WafError('Feature %r not supported by protoc extra' % self.features) 211 212 tsk = self.create_task('protoc', node, out_nodes) 213 tsk.env.append_value('PROTOC_FLAGS', protoc_flags) 214 215 if 'javac' in self.features: 216 self.javac_task.set_run_after(tsk) 217 218 # Instruct protoc where to search for .proto included files. 219 # For C++ standard include files dirs are used, 220 # but this doesn't apply to Python for example 221 for incpath in getattr(self, 'protoc_includes', []): 222 incdirs.append(self.path.find_node(incpath).bldpath()) 223 tsk.env.PROTOC_INCPATHS = incdirs 224 225 # PR2115: protoc generates output of .proto files in nested 226 # directories by canonicalizing paths. To avoid this we have to pass 227 # as first include the full directory file of the .proto file 228 tsk.env.prepend_value('INCPATHS', node.parent.bldpath()) 229 230 use = getattr(self, 'use', '') 231 if not 'PROTOBUF' in use: 232 self.use = self.to_list(use) + ['PROTOBUF'] 233 234def configure(conf): 235 conf.check_cfg(package='protobuf', uselib_store='PROTOBUF', args=['--cflags', '--libs']) 236 conf.find_program('protoc', var='PROTOC') 237 conf.start_msg('Checking for protoc version') 238 protocver = conf.cmd_and_log(conf.env.PROTOC + ['--version'], output=Context.BOTH) 239 protocver = ''.join(protocver).strip()[protocver[0].rfind(' ')+1:] 240 conf.end_msg(protocver) 241 conf.env.PROTOC_MAJOR = protocver[:protocver.find('.')] 242 conf.env.PROTOC_ST = '-I%s' 243 conf.env.PROTOC_FL = '%s' 244