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, Logs
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
70Protoc includes passed via protoc_includes are either relative to the taskgen
71or to the project and are searched in this order.
72
73Include directories external to the waf project can also be passed to the
74extra by using protoc_extincludes
75
76                protoc_extincludes = ['/usr/include/pblib']
77
78
79Notes when using this tool:
80
81- protoc command line parsing is tricky.
82
83  The generated files can be put in subfolders which depend on
84  the order of the include paths.
85
86  Try to be simple when creating task generators
87  containing protoc stuff.
88
89"""
90
91class protoc(Task):
92	run_str = '${PROTOC} ${PROTOC_FL:PROTOC_FLAGS} ${PROTOC_ST:INCPATHS} ${PROTOC_ST:PROTOC_INCPATHS} ${PROTOC_ST:PROTOC_EXTINCPATHS} ${SRC[0].bldpath()}'
93	color   = 'BLUE'
94	ext_out = ['.h', 'pb.cc', '.py', '.java']
95	def scan(self):
96		"""
97		Scan .proto dependencies
98		"""
99		node = self.inputs[0]
100
101		nodes = []
102		names = []
103		seen = []
104		search_nodes = []
105
106		if not node:
107			return (nodes, names)
108
109		if 'cxx' in self.generator.features:
110			search_nodes = self.generator.includes_nodes
111
112		if 'py' in self.generator.features or 'javac' in self.generator.features:
113			for incpath in getattr(self.generator, 'protoc_includes', []):
114				incpath_node = self.generator.path.find_node(incpath)
115				if incpath_node:
116					search_nodes.append(incpath_node)
117				else:
118					# Check if relative to top-level for extra tg dependencies
119					incpath_node = self.generator.bld.path.find_node(incpath)
120					if incpath_node:
121						search_nodes.append(incpath_node)
122					else:
123						raise Errors.WafError('protoc: include path %r does not exist' % incpath)
124
125
126		def parse_node(node):
127			if node in seen:
128				return
129			seen.append(node)
130			code = node.read().splitlines()
131			for line in code:
132				m = re.search(r'^import\s+"(.*)";.*(//)?.*', line)
133				if m:
134					dep = m.groups()[0]
135					for incnode in search_nodes:
136						found = incnode.find_resource(dep)
137						if found:
138							nodes.append(found)
139							parse_node(found)
140						else:
141							names.append(dep)
142
143		parse_node(node)
144		# Add also dependencies path to INCPATHS so protoc will find the included file
145		for deppath in nodes:
146			self.env.append_unique('INCPATHS', deppath.parent.bldpath())
147		return (nodes, names)
148
149@extension('.proto')
150def process_protoc(self, node):
151	incdirs = []
152	out_nodes = []
153	protoc_flags = []
154
155	# ensure PROTOC_FLAGS is a list; a copy is used below anyway
156	self.env.PROTOC_FLAGS = self.to_list(self.env.PROTOC_FLAGS)
157
158	if 'cxx' in self.features:
159		cpp_node = node.change_ext('.pb.cc')
160		hpp_node = node.change_ext('.pb.h')
161		self.source.append(cpp_node)
162		out_nodes.append(cpp_node)
163		out_nodes.append(hpp_node)
164		protoc_flags.append('--cpp_out=%s' % node.parent.get_bld().bldpath())
165
166	if 'py' in self.features:
167		py_node = node.change_ext('_pb2.py')
168		self.source.append(py_node)
169		out_nodes.append(py_node)
170		protoc_flags.append('--python_out=%s' % node.parent.get_bld().bldpath())
171
172	if 'javac' in self.features:
173		# Make javac get also pick java code generated in build
174		if not node.parent.get_bld() in self.javac_task.srcdir:
175			self.javac_task.srcdir.append(node.parent.get_bld())
176
177		protoc_flags.append('--java_out=%s' % node.parent.get_bld().bldpath())
178		node.parent.get_bld().mkdir()
179
180	tsk = self.create_task('protoc', node, out_nodes)
181	tsk.env.append_value('PROTOC_FLAGS', protoc_flags)
182
183	if 'javac' in self.features:
184		self.javac_task.set_run_after(tsk)
185
186	# Instruct protoc where to search for .proto included files.
187	# For C++ standard include files dirs are used,
188	# but this doesn't apply to Python for example
189	for incpath in getattr(self, 'protoc_includes', []):
190		incpath_node = self.path.find_node(incpath)
191		if incpath_node:
192			incdirs.append(incpath_node.bldpath())
193		else:
194			# Check if relative to top-level for extra tg dependencies
195			incpath_node = self.bld.path.find_node(incpath)
196			if incpath_node:
197				incdirs.append(incpath_node.bldpath())
198			else:
199				raise Errors.WafError('protoc: include path %r does not exist' % incpath)
200
201	tsk.env.PROTOC_INCPATHS = incdirs
202
203	# Include paths external to the waf project (ie. shared pb repositories)
204	tsk.env.PROTOC_EXTINCPATHS = getattr(self, 'protoc_extincludes', [])
205
206	# PR2115: protoc generates output of .proto files in nested
207	# directories  by canonicalizing paths. To avoid this we have to pass
208	# as first include the full directory file of the .proto file
209	tsk.env.prepend_value('INCPATHS', node.parent.bldpath())
210
211	use = getattr(self, 'use', '')
212	if not 'PROTOBUF' in use:
213		self.use = self.to_list(use) + ['PROTOBUF']
214
215def configure(conf):
216	conf.check_cfg(package='protobuf', uselib_store='PROTOBUF', args=['--cflags', '--libs'])
217	conf.find_program('protoc', var='PROTOC')
218	conf.start_msg('Checking for protoc version')
219	protocver = conf.cmd_and_log(conf.env.PROTOC + ['--version'], output=Context.BOTH)
220	protocver = ''.join(protocver).strip()[protocver[0].rfind(' ')+1:]
221	conf.end_msg(protocver)
222	conf.env.PROTOC_MAJOR = protocver[:protocver.find('.')]
223	conf.env.PROTOC_ST = '-I%s'
224	conf.env.PROTOC_FL = '%s'
225