1# The MIT License (MIT)
2# Copyright © 2018 Jeff Hodges <jeff@somethingsimilar.com>
3
4# Permission is hereby granted, free of charge, to any person obtaining a copy
5# of this software and associated documentation files (the “Software”), to deal
6# in the Software without restriction, including without limitation the rights
7# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8# copies of the Software, and to permit persons to whom the Software is
9# furnished to do so, subject to the following conditions:
10
11# The above copyright notice and this permission notice shall be included in
12# all copies or substantial portions of the Software.
13
14# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20# THE SOFTWARE.
21
22# The rules in this files are still under development. Breaking changes are planned.
23# DO NOT USE IT.
24
25load("//go/private:context.bzl", "go_context")
26load("//go/private:common.bzl", "GO_TOOLCHAIN", "GO_TOOLCHAIN_LABEL")
27load("//go/private/rules:wrappers.bzl", go_binary = "go_binary_macro")
28load("//go/private:providers.bzl", "GoLibrary")
29load("@bazel_skylib//lib:paths.bzl", "paths")
30
31_MOCKGEN_TOOL = Label("//extras/gomock:mockgen")
32_MOCKGEN_MODEL_LIB = Label("//extras/gomock:mockgen_model")
33
34def _gomock_source_impl(ctx):
35 go_ctx = go_context(ctx)
36
37 # In Source mode, it's not necessary to pass through a library, as the only thing we use it for is setting up
38 # the relative file locations. Forcing users to pass a library makes it difficult in the case where a mock should
39 # be included as part of that same library, as it results in a dependency loop (GoMock -> GoLibrary -> GoMock).
40 # Allowing users to pass an importpath directly bypasses this issue.
41 # See the test case in //tests/extras/gomock/source_with_importpath for an example.
42 importpath = ctx.attr.source_importpath if ctx.attr.source_importpath else ctx.attr.library[GoLibrary].importmap
43
44 # create GOPATH and copy source into GOPATH
45 go_path_prefix = "gopath"
46 source_relative_path = paths.join("src", importpath, ctx.file.source.basename)
47 source = ctx.actions.declare_file(paths.join("gopath", source_relative_path))
48
49 # trim the relative path of source to get GOPATH
50 gopath = source.path[:-len(source_relative_path)]
51 ctx.actions.run_shell(
52 outputs = [source],
53 inputs = [ctx.file.source],
54 command = "mkdir -p {0} && cp -L {1} {0}".format(source.dirname, ctx.file.source.path),
55 )
56
57 # passed in source needs to be in gopath to not trigger module mode
58 args = ["-source", source.path]
59
60 args, needed_files = _handle_shared_args(ctx, args)
61
62 if len(ctx.attr.aux_files) > 0:
63 aux_files = []
64 for target, pkg in ctx.attr.aux_files.items():
65 f = target.files.to_list()[0]
66 aux = ctx.actions.declare_file(paths.join(go_path_prefix, "src", pkg, f.basename))
67 ctx.actions.run_shell(
68 outputs = [aux],
69 inputs = [f],
70 command = "mkdir -p {0} && cp -L {1} {0}".format(aux.dirname, f.path),
71 )
72 aux_files.append("{0}={1}".format(pkg, aux.path))
73 needed_files.append(aux)
74 args += ["-aux_files", ",".join(aux_files)]
75
76 inputs = (
77 needed_files +
78 go_ctx.sdk.headers + go_ctx.sdk.srcs + go_ctx.sdk.tools
79 ) + [source]
80
81 # We can use the go binary from the stdlib for most of the environment
82 # variables, but our GOPATH is specific to the library target we were given.
83 ctx.actions.run_shell(
84 outputs = [ctx.outputs.out],
85 inputs = inputs,
86 tools = [
87 ctx.file.mockgen_tool,
88 go_ctx.go,
89 ],
90 toolchain = GO_TOOLCHAIN_LABEL,
91 command = """
92 export GOPATH=$(pwd)/{gopath} &&
93 export GOROOT=$(pwd)/{goroot} &&
94 {cmd} {args} > {out}
95 """.format(
96 gopath = gopath,
97 goroot = go_ctx.sdk.root_file.dirname,
98 cmd = "$(pwd)/" + ctx.file.mockgen_tool.path,
99 args = " ".join(args),
100 out = ctx.outputs.out.path,
101 mnemonic = "GoMockSourceGen",
102 ),
103 env = {
104 # GOCACHE is required starting in Go 1.12
105 "GOCACHE": "./.gocache",
106 # gomock runs in the special GOPATH environment
107 "GO111MODULE": "off",
108 },
109 )
110
111_gomock_source = rule(
112 _gomock_source_impl,
113 attrs = {
114 "library": attr.label(
115 doc = "The target the Go library where this source file belongs",
116 providers = [GoLibrary],
117 mandatory = False,
118 ),
119 "source_importpath": attr.string(
120 doc = "The importpath for the source file. Alternative to passing library, which can lead to circular dependencies between mock and library targets.",
121 mandatory = False,
122 ),
123 "source": attr.label(
124 doc = "A Go source file to find all the interfaces to generate mocks for. See also the docs for library.",
125 mandatory = False,
126 allow_single_file = True,
127 ),
128 "out": attr.output(
129 doc = "The new Go file to emit the generated mocks into",
130 mandatory = True,
131 ),
132 "aux_files": attr.label_keyed_string_dict(
133 default = {},
134 doc = "A map from auxilliary Go source files to their packages.",
135 allow_files = True,
136 ),
137 "package": attr.string(
138 doc = "The name of the package the generated mocks should be in. If not specified, uses mockgen's default.",
139 ),
140 "self_package": attr.string(
141 doc = "The full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. This can happen if the mock's package is set to one of its inputs (usually the main one) and the output is stdio so mockgen cannot detect the final output package. Setting this flag will then tell mockgen which import to exclude.",
142 ),
143 "imports": attr.string_dict(
144 doc = "Dictionary of name-path pairs of explicit imports to use.",
145 ),
146 "mock_names": attr.string_dict(
147 doc = "Dictionary of interface name to mock name pairs to change the output names of the mock objects. Mock names default to 'Mock' prepended to the name of the interface.",
148 default = {},
149 ),
150 "copyright_file": attr.label(
151 doc = "Optional file containing copyright to prepend to the generated contents.",
152 allow_single_file = True,
153 mandatory = False,
154 ),
155 "mockgen_tool": attr.label(
156 doc = "The mockgen tool to run",
157 default = _MOCKGEN_TOOL,
158 allow_single_file = True,
159 executable = True,
160 cfg = "exec",
161 mandatory = False,
162 ),
163 "_go_context_data": attr.label(
164 default = "//:go_context_data",
165 ),
166 },
167 toolchains = [GO_TOOLCHAIN],
168)
169
170def gomock(name, out, library = None, source_importpath = "", source = None, interfaces = [], package = "", self_package = "", aux_files = {}, mockgen_tool = _MOCKGEN_TOOL, imports = {}, copyright_file = None, mock_names = {}, **kwargs):
171 """Calls [mockgen](https://github.com/golang/mock) to generates a Go file containing mocks from the given library.
172
173 If `source` is given, the mocks are generated in source mode; otherwise in reflective mode.
174
175 Args:
176 name: the target name.
177 out: the output Go file name.
178 library: the Go library to look into for the interfaces (reflective mode) or source (source mode). If running in source mode, you can specify source_importpath instead of this parameter.
179 source_importpath: the importpath for the source file. Alternative to passing library, which can lead to circular dependencies between mock and library targets. Only valid for source mode.
180 source: a Go file in the given `library`. If this is given, `gomock` will call mockgen in source mode to mock all interfaces in the file.
181 interfaces: a list of interfaces in the given `library` to be mocked in reflective mode.
182 package: the name of the package the generated mocks should be in. If not specified, uses mockgen's default. See [mockgen's -package](https://github.com/golang/mock#flags) for more information.
183 self_package: the full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. See [mockgen's -self_package](https://github.com/golang/mock#flags) for more information.
184 aux_files: a map from source files to their package path. This only needed when `source` is provided. See [mockgen's -aux_files](https://github.com/golang/mock#flags) for more information.
185 mockgen_tool: the mockgen tool to run.
186 imports: dictionary of name-path pairs of explicit imports to use. See [mockgen's -imports](https://github.com/golang/mock#flags) for more information.
187 copyright_file: optional file containing copyright to prepend to the generated contents. See [mockgen's -copyright_file](https://github.com/golang/mock#flags) for more information.
188 mock_names: dictionary of interface name to mock name pairs to change the output names of the mock objects. Mock names default to 'Mock' prepended to the name of the interface. See [mockgen's -mock_names](https://github.com/golang/mock#flags) for more information.
189 kwargs: common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) to all Bazel rules.
190 """
191 if source:
192 _gomock_source(
193 name = name,
194 out = out,
195 library = library,
196 source_importpath = source_importpath,
197 source = source,
198 package = package,
199 self_package = self_package,
200 aux_files = aux_files,
201 mockgen_tool = mockgen_tool,
202 imports = imports,
203 copyright_file = copyright_file,
204 mock_names = mock_names,
205 **kwargs
206 )
207 else:
208 _gomock_reflect(
209 name = name,
210 out = out,
211 library = library,
212 interfaces = interfaces,
213 package = package,
214 self_package = self_package,
215 mockgen_tool = mockgen_tool,
216 imports = imports,
217 copyright_file = copyright_file,
218 mock_names = mock_names,
219 **kwargs
220 )
221
222def _gomock_reflect(name, library, out, mockgen_tool, **kwargs):
223 interfaces = kwargs.pop("interfaces", None)
224 mockgen_model_lib = kwargs.pop("mockgen_model_library", _MOCKGEN_MODEL_LIB)
225
226 prog_src = name + "_gomock_prog"
227 prog_src_out = prog_src + ".go"
228 _gomock_prog_gen(
229 name = prog_src,
230 interfaces = interfaces,
231 library = library,
232 out = prog_src_out,
233 mockgen_tool = mockgen_tool,
234 )
235 prog_bin = name + "_gomock_prog_bin"
236 go_binary(
237 name = prog_bin,
238 srcs = [prog_src_out],
239 deps = [library, mockgen_model_lib],
240 )
241 _gomock_prog_exec(
242 name = name,
243 interfaces = interfaces,
244 library = library,
245 out = out,
246 prog_bin = prog_bin,
247 mockgen_tool = mockgen_tool,
248 **kwargs
249 )
250
251def _gomock_prog_gen_impl(ctx):
252 args = ["-prog_only"]
253 args.append(ctx.attr.library[GoLibrary].importpath)
254 args.append(",".join(ctx.attr.interfaces))
255
256 cmd = ctx.file.mockgen_tool
257 out = ctx.outputs.out
258 ctx.actions.run_shell(
259 outputs = [out],
260 tools = [cmd],
261 command = """
262 {cmd} {args} > {out}
263 """.format(
264 cmd = "$(pwd)/" + cmd.path,
265 args = " ".join(args),
266 out = out.path,
267 ),
268 mnemonic = "GoMockReflectProgOnlyGen",
269 )
270
271_gomock_prog_gen = rule(
272 _gomock_prog_gen_impl,
273 attrs = {
274 "library": attr.label(
275 doc = "The target the Go library is at to look for the interfaces in. When this is set and source is not set, mockgen will use its reflect code to generate the mocks. If source is set, its dependencies will be included in the GOPATH that mockgen will be run in.",
276 providers = [GoLibrary],
277 mandatory = True,
278 ),
279 "out": attr.output(
280 doc = "The new Go source file put the mock generator code",
281 mandatory = True,
282 ),
283 "interfaces": attr.string_list(
284 allow_empty = False,
285 doc = "The names of the Go interfaces to generate mocks for. If not set, all of the interfaces in the library or source file will have mocks generated for them.",
286 mandatory = True,
287 ),
288 "mockgen_tool": attr.label(
289 doc = "The mockgen tool to run",
290 default = _MOCKGEN_TOOL,
291 allow_single_file = True,
292 executable = True,
293 cfg = "exec",
294 mandatory = False,
295 ),
296 "_go_context_data": attr.label(
297 default = "//:go_context_data",
298 ),
299 },
300 toolchains = [GO_TOOLCHAIN],
301)
302
303def _gomock_prog_exec_impl(ctx):
304 args = ["-exec_only", ctx.file.prog_bin.path]
305 args, needed_files = _handle_shared_args(ctx, args)
306
307 # annoyingly, the interfaces join has to go after the importpath so we can't
308 # share those.
309 args.append(ctx.attr.library[GoLibrary].importpath)
310 args.append(",".join(ctx.attr.interfaces))
311
312 ctx.actions.run_shell(
313 outputs = [ctx.outputs.out],
314 inputs = [ctx.file.prog_bin] + needed_files,
315 tools = [ctx.file.mockgen_tool],
316 command = """{cmd} {args} > {out}""".format(
317 cmd = "$(pwd)/" + ctx.file.mockgen_tool.path,
318 args = " ".join(args),
319 out = ctx.outputs.out.path,
320 ),
321 env = {
322 # GOCACHE is required starting in Go 1.12
323 "GOCACHE": "./.gocache",
324 },
325 mnemonic = "GoMockReflectExecOnlyGen",
326 )
327
328_gomock_prog_exec = rule(
329 _gomock_prog_exec_impl,
330 attrs = {
331 "library": attr.label(
332 doc = "The target the Go library is at to look for the interfaces in. When this is set and source is not set, mockgen will use its reflect code to generate the mocks. If source is set, its dependencies will be included in the GOPATH that mockgen will be run in.",
333 providers = [GoLibrary],
334 mandatory = True,
335 ),
336 "out": attr.output(
337 doc = "The new Go source file to put the generated mock code",
338 mandatory = True,
339 ),
340 "interfaces": attr.string_list(
341 allow_empty = False,
342 doc = "The names of the Go interfaces to generate mocks for. If not set, all of the interfaces in the library or source file will have mocks generated for them.",
343 mandatory = True,
344 ),
345 "package": attr.string(
346 doc = "The name of the package the generated mocks should be in. If not specified, uses mockgen's default.",
347 ),
348 "self_package": attr.string(
349 doc = "The full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. This can happen if the mock's package is set to one of its inputs (usually the main one) and the output is stdio so mockgen cannot detect the final output package. Setting this flag will then tell mockgen which import to exclude.",
350 ),
351 "imports": attr.string_dict(
352 doc = "Dictionary of name-path pairs of explicit imports to use.",
353 ),
354 "mock_names": attr.string_dict(
355 doc = "Dictionary of interfaceName-mockName pairs of explicit mock names to use. Mock names default to 'Mock'+ interfaceName suffix.",
356 default = {},
357 ),
358 "copyright_file": attr.label(
359 doc = "Optional file containing copyright to prepend to the generated contents.",
360 allow_single_file = True,
361 mandatory = False,
362 ),
363 "prog_bin": attr.label(
364 doc = "The program binary generated by mockgen's -prog_only and compiled by bazel.",
365 allow_single_file = True,
366 executable = True,
367 cfg = "exec",
368 mandatory = True,
369 ),
370 "mockgen_tool": attr.label(
371 doc = "The mockgen tool to run",
372 default = _MOCKGEN_TOOL,
373 allow_single_file = True,
374 executable = True,
375 cfg = "exec",
376 mandatory = False,
377 ),
378 "_go_context_data": attr.label(
379 default = "//:go_context_data",
380 ),
381 },
382 toolchains = [GO_TOOLCHAIN],
383)
384
385def _handle_shared_args(ctx, args):
386 needed_files = []
387
388 if ctx.attr.package != "":
389 args += ["-package", ctx.attr.package]
390 if ctx.attr.self_package != "":
391 args += ["-self_package", ctx.attr.self_package]
392 if len(ctx.attr.imports) > 0:
393 imports = ",".join(["{0}={1}".format(name, pkg) for name, pkg in ctx.attr.imports.items()])
394 args += ["-imports", imports]
395 if ctx.file.copyright_file != None:
396 args += ["-copyright_file", ctx.file.copyright_file.path]
397 needed_files.append(ctx.file.copyright_file)
398 if len(ctx.attr.mock_names) > 0:
399 mock_names = ",".join(["{0}={1}".format(name, pkg) for name, pkg in ctx.attr.mock_names.items()])
400 args += ["-mock_names", mock_names]
401
402 return args, needed_files
View as plain text