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