...

Text file src/github.com/bazelbuild/rules_go/extras/gomock.bzl

Documentation: github.com/bazelbuild/rules_go/extras

     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