xref: /aosp_15_r20/external/bazelbuild-rules_go/extras/gomock.bzl (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1# The MIT License (MIT)
2# Copyright © 2018 Jeff Hodges <[email protected]>
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:go_toolchain.bzl", "GO_TOOLCHAIN")
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    # create GOPATH and copy source into GOPATH
38    source_relative_path = paths.join("src", ctx.attr.library[GoLibrary].importmap, ctx.file.source.basename)
39    source = ctx.actions.declare_file(paths.join("gopath", source_relative_path))
40
41    # trim the relative path of source to get GOPATH
42    gopath = source.path[:-len(source_relative_path)]
43    ctx.actions.run_shell(
44        outputs = [source],
45        inputs = [ctx.file.source],
46        command = "mkdir -p {0} && cp -L {1} {0}".format(source.dirname, ctx.file.source.path),
47    )
48
49    # passed in source needs to be in gopath to not trigger module mode
50    args = ["-source", source.path]
51
52    args, needed_files = _handle_shared_args(ctx, args)
53
54    if len(ctx.attr.aux_files) > 0:
55        aux_files = []
56        for target, pkg in ctx.attr.aux_files.items():
57            f = target.files.to_list()[0]
58            aux = ctx.actions.declare_file(paths.join(gopath, "src", pkg, f.basename))
59            ctx.actions.run_shell(
60                outputs = [aux],
61                inputs = [f],
62                command = "mkdir -p {0} && cp -L {1} {0}".format(aux.dirname, f.path),
63            )
64            aux_files.append("{0}={1}".format(pkg, aux.path))
65            needed_files.append(f)
66        args += ["-aux_files", ",".join(aux_files)]
67
68    inputs = (
69        needed_files +
70        go_ctx.sdk.headers + go_ctx.sdk.srcs + go_ctx.sdk.tools
71    ) + [source]
72
73    # We can use the go binary from the stdlib for most of the environment
74    # variables, but our GOPATH is specific to the library target we were given.
75    ctx.actions.run_shell(
76        outputs = [ctx.outputs.out],
77        inputs = inputs,
78        tools = [
79            ctx.file.mockgen_tool,
80            go_ctx.go,
81        ],
82        command = """
83            export GOPATH=$(pwd)/{gopath} &&
84            {cmd} {args} > {out}
85        """.format(
86            gopath = gopath,
87            cmd = "$(pwd)/" + ctx.file.mockgen_tool.path,
88            args = " ".join(args),
89            out = ctx.outputs.out.path,
90            mnemonic = "GoMockSourceGen",
91        ),
92        env = {
93            # GOCACHE is required starting in Go 1.12
94            "GOCACHE": "./.gocache",
95        },
96    )
97
98_gomock_source = rule(
99    _gomock_source_impl,
100    attrs = {
101        "library": attr.label(
102            doc = "The target the Go library where this source file belongs",
103            providers = [GoLibrary],
104            mandatory = True,
105        ),
106        "source": attr.label(
107            doc = "A Go source file to find all the interfaces to generate mocks for. See also the docs for library.",
108            mandatory = False,
109            allow_single_file = True,
110        ),
111        "out": attr.output(
112            doc = "The new Go file to emit the generated mocks into",
113            mandatory = True,
114        ),
115        "aux_files": attr.label_keyed_string_dict(
116            default = {},
117            doc = "A map from auxilliary Go source files to their packages.",
118            allow_files = True,
119        ),
120        "package": attr.string(
121            doc = "The name of the package the generated mocks should be in. If not specified, uses mockgen's default.",
122        ),
123        "self_package": attr.string(
124            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.",
125        ),
126        "imports": attr.string_dict(
127            doc = "Dictionary of name-path pairs of explicit imports to use.",
128        ),
129        "mock_names": attr.string_dict(
130            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.",
131            default = {},
132        ),
133        "copyright_file": attr.label(
134            doc = "Optional file containing copyright to prepend to the generated contents.",
135            allow_single_file = True,
136            mandatory = False,
137        ),
138        "mockgen_tool": attr.label(
139            doc = "The mockgen tool to run",
140            default = _MOCKGEN_TOOL,
141            allow_single_file = True,
142            executable = True,
143            cfg = "exec",
144            mandatory = False,
145        ),
146        "_go_context_data": attr.label(
147            default = "//:go_context_data",
148        ),
149    },
150    toolchains = [GO_TOOLCHAIN],
151)
152
153def gomock(name, library, out, source = None, interfaces = [], package = "", self_package = "", aux_files = {}, mockgen_tool = _MOCKGEN_TOOL, imports = {}, copyright_file = None, mock_names = {}, **kwargs):
154    """Calls [mockgen](https://github.com/golang/mock) to generates a Go file containing mocks from the given library.
155
156    If `source` is given, the mocks are generated in source mode; otherwise in reflective mode.
157
158    Args:
159        name: the target name.
160        library: the Go library to took for the interfaces (reflecitve mode) or source (source mode).
161        out: the output Go file name.
162        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.
163        interfaces: a list of interfaces in the given `library` to be mocked in reflective mode.
164        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.
165        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.
166        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.
167        mockgen_tool: the mockgen tool to run.
168        imports: dictionary of name-path pairs of explicit imports to use. See [mockgen's -imports](https://github.com/golang/mock#flags) for more information.
169        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.
170        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.
171        kwargs: common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) to all Bazel rules.
172    """
173    if source:
174        _gomock_source(
175            name = name,
176            library = library,
177            out = out,
178            source = source,
179            package = package,
180            self_package = self_package,
181            aux_files = aux_files,
182            mockgen_tool = mockgen_tool,
183            imports = imports,
184            copyright_file = copyright_file,
185            mock_names = mock_names,
186            **kwargs
187        )
188    else:
189        _gomock_reflect(
190            name = name,
191            library = library,
192            out = out,
193            interfaces = interfaces,
194            package = package,
195            self_package = self_package,
196            mockgen_tool = mockgen_tool,
197            imports = imports,
198            copyright_file = copyright_file,
199            mock_names = mock_names,
200            **kwargs
201        )
202
203def _gomock_reflect(name, library, out, mockgen_tool, **kwargs):
204    interfaces = kwargs.pop("interfaces", None)
205
206    mockgen_model_lib = _MOCKGEN_MODEL_LIB
207    if kwargs.get("mockgen_model_library", None):
208        mockgen_model_lib = kwargs["mockgen_model_library"]
209
210    prog_src = name + "_gomock_prog"
211    prog_src_out = prog_src + ".go"
212    _gomock_prog_gen(
213        name = prog_src,
214        interfaces = interfaces,
215        library = library,
216        out = prog_src_out,
217        mockgen_tool = mockgen_tool,
218    )
219    prog_bin = name + "_gomock_prog_bin"
220    go_binary(
221        name = prog_bin,
222        srcs = [prog_src_out],
223        deps = [library, mockgen_model_lib],
224    )
225    _gomock_prog_exec(
226        name = name,
227        interfaces = interfaces,
228        library = library,
229        out = out,
230        prog_bin = prog_bin,
231        mockgen_tool = mockgen_tool,
232        **kwargs
233    )
234
235def _gomock_prog_gen_impl(ctx):
236    args = ["-prog_only"]
237    args.append(ctx.attr.library[GoLibrary].importpath)
238    args.append(",".join(ctx.attr.interfaces))
239
240    cmd = ctx.file.mockgen_tool
241    out = ctx.outputs.out
242    ctx.actions.run_shell(
243        outputs = [out],
244        tools = [cmd],
245        command = """
246           {cmd} {args} > {out}
247        """.format(
248            cmd = "$(pwd)/" + cmd.path,
249            args = " ".join(args),
250            out = out.path,
251        ),
252        mnemonic = "GoMockReflectProgOnlyGen",
253    )
254
255_gomock_prog_gen = rule(
256    _gomock_prog_gen_impl,
257    attrs = {
258        "library": attr.label(
259            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.",
260            providers = [GoLibrary],
261            mandatory = True,
262        ),
263        "out": attr.output(
264            doc = "The new Go source file put the mock generator code",
265            mandatory = True,
266        ),
267        "interfaces": attr.string_list(
268            allow_empty = False,
269            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.",
270            mandatory = True,
271        ),
272        "mockgen_tool": attr.label(
273            doc = "The mockgen tool to run",
274            default = _MOCKGEN_TOOL,
275            allow_single_file = True,
276            executable = True,
277            cfg = "exec",
278            mandatory = False,
279        ),
280        "_go_context_data": attr.label(
281            default = "//:go_context_data",
282        ),
283    },
284    toolchains = [GO_TOOLCHAIN],
285)
286
287def _gomock_prog_exec_impl(ctx):
288    args = ["-exec_only", ctx.file.prog_bin.path]
289    args, needed_files = _handle_shared_args(ctx, args)
290
291    # annoyingly, the interfaces join has to go after the importpath so we can't
292    # share those.
293    args.append(ctx.attr.library[GoLibrary].importpath)
294    args.append(",".join(ctx.attr.interfaces))
295
296    ctx.actions.run_shell(
297        outputs = [ctx.outputs.out],
298        inputs = [ctx.file.prog_bin] + needed_files,
299        tools = [ctx.file.mockgen_tool],
300        command = """{cmd} {args} > {out}""".format(
301            cmd = "$(pwd)/" + ctx.file.mockgen_tool.path,
302            args = " ".join(args),
303            out = ctx.outputs.out.path,
304        ),
305        env = {
306            # GOCACHE is required starting in Go 1.12
307            "GOCACHE": "./.gocache",
308        },
309        mnemonic = "GoMockReflectExecOnlyGen",
310    )
311
312_gomock_prog_exec = rule(
313    _gomock_prog_exec_impl,
314    attrs = {
315        "library": attr.label(
316            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.",
317            providers = [GoLibrary],
318            mandatory = True,
319        ),
320        "out": attr.output(
321            doc = "The new Go source file to put the generated mock code",
322            mandatory = True,
323        ),
324        "interfaces": attr.string_list(
325            allow_empty = False,
326            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.",
327            mandatory = True,
328        ),
329        "package": attr.string(
330            doc = "The name of the package the generated mocks should be in. If not specified, uses mockgen's default.",
331        ),
332        "self_package": attr.string(
333            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.",
334        ),
335        "imports": attr.string_dict(
336            doc = "Dictionary of name-path pairs of explicit imports to use.",
337        ),
338        "mock_names": attr.string_dict(
339            doc = "Dictionary of interfaceName-mockName pairs of explicit mock names to use. Mock names default to 'Mock'+ interfaceName suffix.",
340            default = {},
341        ),
342        "copyright_file": attr.label(
343            doc = "Optional file containing copyright to prepend to the generated contents.",
344            allow_single_file = True,
345            mandatory = False,
346        ),
347        "prog_bin": attr.label(
348            doc = "The program binary generated by mockgen's -prog_only and compiled by bazel.",
349            allow_single_file = True,
350            executable = True,
351            cfg = "exec",
352            mandatory = True,
353        ),
354        "mockgen_tool": attr.label(
355            doc = "The mockgen tool to run",
356            default = _MOCKGEN_TOOL,
357            allow_single_file = True,
358            executable = True,
359            cfg = "exec",
360            mandatory = False,
361        ),
362        "_go_context_data": attr.label(
363            default = "//:go_context_data",
364        ),
365    },
366    toolchains = [GO_TOOLCHAIN],
367)
368
369def _handle_shared_args(ctx, args):
370    needed_files = []
371
372    if ctx.attr.package != "":
373        args += ["-package", ctx.attr.package]
374    if ctx.attr.self_package != "":
375        args += ["-self_package", ctx.attr.self_package]
376    if len(ctx.attr.imports) > 0:
377        imports = ",".join(["{0}={1}".format(name, pkg) for name, pkg in ctx.attr.imports.items()])
378        args += ["-imports", imports]
379    if ctx.file.copyright_file != None:
380        args += ["-copyright_file", ctx.file.copyright_file.path]
381        needed_files.append(ctx.file.copyright_file)
382    if len(ctx.attr.mock_names) > 0:
383        mock_names = ",".join(["{0}={1}".format(name, pkg) for name, pkg in ctx.attr.mock_names.items()])
384        args += ["-mock_names", mock_names]
385
386    return args, needed_files
387