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