1# Copyright 2014 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15load("//internal:common.bzl", "env_execute", "executable_extension")
16load("//internal:go_repository_cache.bzl", "read_cache_env")
17load("@bazel_tools//tools/build_defs/repo:utils.bzl", "patch", "read_user_netrc", "use_netrc")
18
19_DOC = """
20`go_repository` downloads a Go project and generates build files with Gazelle
21if they are not already present. This is the simplest way to depend on
22external Go projects.
23
24When `go_repository` is in module mode, it saves downloaded modules in a shared,
25internal cache within Bazel's cache. It may be cleared with `bazel clean --expunge`.
26By setting the environment variable `GO_REPOSITORY_USE_HOST_CACHE=1`, you can
27force `go_repository` to use the module cache on the host system in the location
28returned by `go env GOPATH`. Alternatively, by setting the environment variable
29`GO_REPOSITORY_USE_HOST_MODCACHE=1`, you can force `go_repository` to use only
30the module cache on the host system in the location returned by `go env GOMODCACHE`.
31
32**Example**
33
34```starlark
35load("@bazel_gazelle//:deps.bzl", "go_repository")
36
37# Download using "go mod download"
38go_repository(
39 name = "com_github_pkg_errors",
40 importpath = "github.com/pkg/errors",
41 sum = "h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=",
42 version = "v0.8.1",
43)
44
45# Download automatically via git
46go_repository(
47 name = "com_github_pkg_errors",
48 commit = "816c9085562cd7ee03e7f8188a1cfd942858cded",
49 importpath = "github.com/pkg/errors",
50)
51
52# Download from git fork
53go_repository(
54 name = "com_github_pkg_errors",
55 commit = "816c9085562cd7ee03e7f8188a1cfd942858cded",
56 importpath = "github.com/pkg/errors",
57 remote = "https://example.com/fork/github.com/pkg/errors",
58 vcs = "git",
59)
60
61# Download via HTTP
62go_repository(
63 name = "com_github_pkg_errors",
64 importpath = "github.com/pkg/errors",
65 urls = ["https://codeload.github.com/pkg/errors/zip/816c9085562cd7ee03e7f8188a1cfd942858cded"],
66 strip_prefix = "errors-816c9085562cd7ee03e7f8188a1cfd942858cded",
67 type = "zip",
68)
69
70# Download major version suffixed via git
71go_repository(
72 name = "com_github_thediveo_enumflag_v2",
73 commit = "0217df583bf3d37b92798602e5061b36556bcd38",
74 importpath = "github.com/thediveo/enumflag/v2",
75 remote = "https://github.com/thediveo/enumflag",
76 vcs = "git",
77)
78```
79
80"""
81
82# copied from
83# https://github.com/bazelbuild/bazel/blob/d273cb62f43ef8169415cf60fc96e503ea2ad823/tools/build_defs/repo/http.bzl#L76
84_AUTH_PATTERN_DOC = """An optional dict mapping host names to custom authorization patterns.
85
86If a URL's host name is present in this dict the value will be used as a pattern when
87generating the authorization header for the http request. This enables the use of custom
88authorization schemes used in a lot of common cloud storage providers.
89
90The pattern currently supports 2 tokens: <code><login></code> and
91<code><password></code>, which are replaced with their equivalent value
92in the netrc file for the same host name. After formatting, the result is set
93as the value for the <code>Authorization</code> field of the HTTP request.
94
95Example attribute and netrc for a http download to an oauth2 enabled API using a bearer token:
96
97<pre>
98auth_patterns = {
99 "storage.cloudprovider.com": "Bearer <password>"
100}
101</pre>
102
103netrc:
104
105<pre>
106machine storage.cloudprovider.com
107 password RANDOM-TOKEN
108</pre>
109
110The final HTTP request would have the following header:
111
112<pre>
113Authorization: Bearer RANDOM-TOKEN
114</pre>
115"""
116
117# We can't disable timeouts on Bazel, but we can set them to large values.
118_GO_REPOSITORY_TIMEOUT = 86400
119
120def _go_repository_impl(ctx):
121 # TODO(#549): vcs repositories are not cached and still need to be fetched.
122 # Download the repository or module.
123 fetch_repo_args = None
124 gazelle_path = None
125
126 # Declare Label dependencies at the top of function to avoid unnecessary fetching:
127 # https://docs.bazel.build/versions/main/skylark/repository_rules.html#when-is-the-implementation-function-executed
128 go_env_cache = str(ctx.path(Label("@bazel_gazelle_go_repository_cache//:go.env")))
129 if not ctx.attr.urls:
130 fetch_repo = str(ctx.path(Label("@bazel_gazelle_go_repository_tools//:bin/fetch_repo{}".format(executable_extension(ctx)))))
131 generate = ctx.attr.build_file_generation == "on"
132 _gazelle = "@bazel_gazelle_go_repository_tools//:bin/gazelle{}".format(executable_extension(ctx))
133 if generate:
134 gazelle_path = ctx.path(Label(_gazelle))
135
136 if ctx.attr.urls:
137 # HTTP mode
138 for key in ("commit", "tag", "vcs", "remote", "version", "sum", "replace"):
139 if getattr(ctx.attr, key):
140 fail("cannot specifiy both urls and %s" % key, key)
141 result = ctx.download_and_extract(
142 url = ctx.attr.urls,
143 sha256 = ctx.attr.sha256,
144 canonical_id = ctx.attr.canonical_id,
145 stripPrefix = ctx.attr.strip_prefix,
146 type = ctx.attr.type,
147 auth = use_netrc(read_user_netrc(ctx), ctx.attr.urls, ctx.attr.auth_patterns),
148 )
149 if not ctx.attr.sha256:
150 print("For Go module \"{path}\", integrity not specified, calculated sha256 = \"{sha256}\"".format(
151 path = ctx.attr.importpath,
152 sha256 = result.sha256,
153 ))
154 elif ctx.attr.commit or ctx.attr.tag:
155 # repository mode
156 if ctx.attr.commit:
157 rev = ctx.attr.commit
158 rev_key = "commit"
159 elif ctx.attr.tag:
160 rev = ctx.attr.tag
161 rev_key = "tag"
162 for key in ("urls", "strip_prefix", "type", "sha256", "version", "sum", "replace", "canonical_id"):
163 if getattr(ctx.attr, key):
164 fail("cannot specify both %s and %s" % (rev_key, key), key)
165
166 if ctx.attr.vcs and not ctx.attr.remote:
167 fail("if vcs is specified, remote must also be")
168
169 fetch_repo_args = ["-dest", ctx.path(""), "-importpath", ctx.attr.importpath]
170 if ctx.attr.remote:
171 fetch_repo_args.extend(["--remote", ctx.attr.remote])
172 if rev:
173 fetch_repo_args.extend(["--rev", rev])
174 if ctx.attr.vcs:
175 fetch_repo_args.extend(["--vcs", ctx.attr.vcs])
176 elif ctx.attr.version:
177 # module mode
178 for key in ("urls", "strip_prefix", "type", "sha256", "commit", "tag", "vcs", "remote"):
179 if getattr(ctx.attr, key):
180 fail("cannot specify both version and %s" % key)
181 if not ctx.attr.sum:
182 fail("if version is specified, sum must also be")
183
184 fetch_path = ctx.attr.replace if ctx.attr.replace else ctx.attr.importpath
185 fetch_repo_args = [
186 "-dest=" + str(ctx.path("")),
187 "-importpath=" + fetch_path,
188 "-version=" + ctx.attr.version,
189 "-sum=" + ctx.attr.sum,
190 ]
191 else:
192 fail("one of urls, commit, tag, or version must be specified")
193
194 env = read_cache_env(ctx, go_env_cache)
195 env_keys = [
196 # Respect user proxy and sumdb settings for privacy.
197 # TODO(jayconrod): gazelle in go_repository mode should probably
198 # not go out to the network at all. This means *the build*
199 # goes out to the network. We tolerate this for downloading
200 # archives, but finding module roots is a bit much.
201 "GOPROXY",
202 "GONOPROXY",
203 "GOPRIVATE",
204 "GOSUMDB",
205 "GONOSUMDB",
206
207 # PATH is needed to locate git and other vcs tools.
208 "PATH",
209
210 # HOME is needed to locate vcs configuration files (.gitconfig).
211 "HOME",
212
213 # Settings below are used by vcs tools.
214 "SSH_AUTH_SOCK",
215 "SSL_CERT_FILE",
216 "SSL_CERT_DIR",
217 "HTTP_PROXY",
218 "HTTPS_PROXY",
219 "NO_PROXY",
220 "http_proxy",
221 "https_proxy",
222 "no_proxy",
223 "GIT_SSL_CAINFO",
224 "GIT_SSH",
225 "GIT_SSH_COMMAND",
226 "GIT_CONFIG_COUNT",
227 ]
228
229 # Git allows passing configuration through environmental variables, this will be picked
230 # by go get properly: https://www.git-scm.com/docs/git-config/#Documentation/git-config.txt-GITCONFIGCOUNT
231 if "GIT_CONFIG_COUNT" in ctx.os.environ:
232 count = ctx.os.environ["GIT_CONFIG_COUNT"]
233 if count:
234 if not count.isdigit or int(count) < 1:
235 fail("GIT_CONFIG_COUNT has to be a positive integer")
236 count = int(count)
237 for i in range(count):
238 key = "GIT_CONFIG_KEY_%d" % i
239 value = "GIT_CONFIG_VALUE_%d" % i
240 for j in [key, value]:
241 if j not in ctx.os.environ:
242 fail("%s is not defined as an environment variable, but you asked for GIT_COUNT_COUNT=%d" % (j, count))
243 env_keys = env_keys + [key, value]
244
245 env.update({k: ctx.os.environ[k] for k in env_keys if k in ctx.os.environ})
246
247 if fetch_repo_args:
248 # Disable sumdb in fetch_repo. In module mode, the sum is a mandatory
249 # attribute of go_repository, so we don't need to look it up.
250 fetch_repo_env = dict(env)
251 fetch_repo_env["GOSUMDB"] = "off"
252
253 # Override external GO111MODULE, because it is needed by module mode, no-op in repository mode
254 fetch_repo_env["GO111MODULE"] = "on"
255
256 result = env_execute(
257 ctx,
258 [fetch_repo] + fetch_repo_args,
259 environment = fetch_repo_env,
260 timeout = _GO_REPOSITORY_TIMEOUT,
261 )
262 if result.return_code:
263 fail("failed to fetch %s: %s" % (ctx.name, result.stderr))
264 if ctx.attr.debug_mode and result.stderr:
265 print("fetch_repo: " + result.stderr)
266
267 # Repositories are fetched. Determine if build file generation is needed.
268 build_file_names = ctx.attr.build_file_name.split(",")
269 existing_build_file = ""
270 for name in build_file_names:
271 path = ctx.path(name)
272 if path.exists and not env_execute(ctx, ["test", "-f", path]).return_code:
273 existing_build_file = name
274 break
275
276 generate = generate or (not existing_build_file and ctx.attr.build_file_generation == "auto")
277
278 if generate:
279 # Build file generation is needed. Populate Gazelle directive at root build file
280 build_file_name = existing_build_file or build_file_names[0]
281 if len(ctx.attr.build_directives) > 0:
282 ctx.file(
283 build_file_name,
284 "\n".join(["# " + d for d in ctx.attr.build_directives]),
285 )
286
287 # Run Gazelle
288 if gazelle_path == None:
289 gazelle_path = ctx.path(Label(_gazelle))
290
291 # ctx.attr.name is the canonical name of this repository, which contains a '~' if and only
292 # if this repository is generated by a module extension rather than an invocation in
293 # WORKSPACE.
294 is_module_extension_repo = "~" in ctx.attr.name
295 if is_module_extension_repo:
296 # TODO: In Bazel 6.3.0 and earlier, there is no way to obtain a label referencing a repo
297 # generated by an extension from within that extension. We thus have to manually
298 # construct such a label pointing to the sibling `_go_repository_config` repo created by
299 # the `go_deps` extension. All extension-generated repos have names of the form
300 # `<prefix>~<name set by the extension>`.
301 extension_repo_prefix = ctx.attr.name.rpartition("~")[0] + "~"
302 repo_config = ctx.path(Label("@@" + extension_repo_prefix + "bazel_gazelle_go_repository_config//:WORKSPACE"))
303 else:
304 repo_config = ctx.path(ctx.attr.build_config)
305 cmd = [
306 gazelle_path,
307 "-go_repository_mode",
308 "-go_prefix",
309 ctx.attr.importpath,
310 "-mode",
311 "fix",
312 "-repo_root",
313 ctx.path(""),
314 "-repo_config",
315 repo_config,
316 ]
317 if ctx.attr.version:
318 cmd.append("-go_repository_module_mode")
319 if ctx.attr.build_file_name:
320 cmd.extend(["-build_file_name", ctx.attr.build_file_name])
321 if ctx.attr.build_tags:
322 cmd.extend(["-build_tags", ",".join(ctx.attr.build_tags)])
323 if ctx.attr.build_external:
324 cmd.extend(["-external", ctx.attr.build_external])
325 if ctx.attr.build_file_proto_mode:
326 cmd.extend(["-proto", ctx.attr.build_file_proto_mode])
327 if ctx.attr.build_naming_convention:
328 cmd.extend(["-go_naming_convention", ctx.attr.build_naming_convention])
329 if is_module_extension_repo:
330 cmd.append("-bzlmod")
331 cmd.extend(ctx.attr.build_extra_args)
332 cmd.append(ctx.path(""))
333 ctx.report_progress("running Gazelle")
334 result = env_execute(ctx, cmd, environment = env, timeout = _GO_REPOSITORY_TIMEOUT)
335 if result.return_code:
336 fail("failed to generate BUILD files for %s: %s" % (
337 ctx.attr.importpath,
338 result.stderr,
339 ))
340 if ctx.attr.debug_mode and result.stderr:
341 print("%s gazelle.stdout: %s" % (ctx.name, result.stdout))
342 print("%s gazelle.stderr: %s" % (ctx.name, result.stderr))
343
344 # Apply patches if necessary.
345 patch(ctx)
346
347go_repository = repository_rule(
348 implementation = _go_repository_impl,
349 doc = _DOC,
350 attrs = {
351 # Fundamental attributes of a go repository
352 "importpath": attr.string(
353 doc = """The Go import path that matches the root directory of this repository.
354
355 In module mode (when `version` is set), this must be the module path. If
356 neither `urls` nor `remote` is specified, `go_repository` will
357 automatically find the true path of the module, applying import path
358 redirection.
359
360 If build files are generated for this repository, libraries will have their
361 `importpath` attributes prefixed with this `importpath` string. """,
362 mandatory = True,
363 ),
364
365 # Attributes for a repository that should be checked out from VCS
366 "commit": attr.string(
367 doc = """If the repository is downloaded using a version control tool, this is the
368 commit or revision to check out. With git, this would be a sha1 commit id.
369 `commit` and `tag` may not both be set.""",
370 ),
371 "tag": attr.string(
372 doc = """If the repository is downloaded using a version control tool, this is the
373 named revision to check out. `commit` and `tag` may not both be set.""",
374 ),
375 "vcs": attr.string(
376 default = "",
377 doc = """One of `"git"`, `"hg"`, `"svn"`, `"bzr"`.
378
379 The version control system to use. This is usually determined automatically,
380 but it may be necessary to set this when `remote` is set and the VCS cannot
381 be inferred. You must have the corresponding tool installed on your host.""",
382 values = [
383 "",
384 "git",
385 "hg",
386 "svn",
387 "bzr",
388 ],
389 ),
390 "remote": attr.string(
391 doc = """The VCS location where the repository should be downloaded from. This is
392 usually inferred from `importpath`, but you can set `remote` to download
393 from a private repository or a fork.""",
394 ),
395
396 # Attributes for a repository that should be downloaded via HTTP.
397 "urls": attr.string_list(
398 doc = """A list of HTTP(S) URLs where an archive containing the project can be
399 downloaded. Bazel will attempt to download from the first URL; the others
400 are mirrors.""",
401 ),
402 "strip_prefix": attr.string(
403 doc = """If the repository is downloaded via HTTP (`urls` is set), this is a
404 directory prefix to strip. See [`http_archive.strip_prefix`].""",
405 ),
406 "type": attr.string(
407 doc = """One of `"zip"`, `"tar.gz"`, `"tgz"`, `"tar.bz2"`, `"tar.xz"`.
408
409 If the repository is downloaded via HTTP (`urls` is set), this is the
410 file format of the repository archive. This is normally inferred from the
411 downloaded file name.""",
412 ),
413 "sha256": attr.string(
414 doc = """If the repository is downloaded via HTTP (`urls` is set), this is the
415 SHA-256 sum of the downloaded archive. When set, Bazel will verify the archive
416 against this sum before extracting it.
417
418 **CAUTION:** Do not use this with services that prepare source archives on
419 demand, such as codeload.github.com. Any minor change in the server software
420 can cause differences in file order, alignment, and compression that break
421 SHA-256 sums.""",
422 ),
423 "canonical_id": attr.string(
424 doc = """If the repository is downloaded via HTTP (`urls` is set) and this is set, restrict cache hits to those cases where the
425 repository was added to the cache with the same canonical id.""",
426 ),
427 "auth_patterns": attr.string_dict(
428 doc = _AUTH_PATTERN_DOC,
429 ),
430
431 # Attributes for a module that should be downloaded with the Go toolchain.
432 "version": attr.string(
433 doc = """If specified, `go_repository` will download the module at this version
434 using `go mod download`. `sum` must also be set. `commit`, `tag`,
435 and `urls` may not be set. """,
436 ),
437 "sum": attr.string(
438 doc = """A hash of the module contents. In module mode, `go_repository` will verify
439 the downloaded module matches this sum. May only be set when `version`
440 is also set.
441
442 A value for `sum` may be found in the `go.sum` file or by running
443 `go mod download -json <module>@<version>`.""",
444 ),
445 "replace": attr.string(
446 doc = """A replacement for the module named by `importpath`. The module named by
447 `replace` will be downloaded at `version` and verified with `sum`.
448
449 NOTE: There is no `go_repository` equivalent to file path `replace`
450 directives. Use `local_repository` instead.""",
451 ),
452
453 # Attributes for a repository that needs automatic build file generation
454 "build_external": attr.string(
455 default = "static",
456 doc = """One of `"external"`, `"static"` or `"vendored"`.
457
458 This sets Gazelle's `-external` command line flag. In `"static"` mode,
459 Gazelle will not call out to the network to resolve imports.
460
461 **NOTE:** This cannot be used to ignore the `vendor` directory in a
462 repository. The `-external` flag only controls how Gazelle resolves
463 imports which are not present in the repository. Use
464 `build_extra_args = ["-exclude=vendor"]` instead.""",
465 values = [
466 "",
467 "external",
468 "static",
469 "vendored",
470 ],
471 ),
472 "build_file_name": attr.string(
473 default = "BUILD.bazel,BUILD",
474 doc = """Comma-separated list of names Gazelle will consider to be build files.
475 If a repository contains files named `build` that aren't related to Bazel,
476 it may help to set this to `"BUILD.bazel"`, especially on case-insensitive
477 file systems.""",
478 ),
479 "build_file_generation": attr.string(
480 default = "auto",
481 doc = """One of `"auto"`, `"on"`, `"off"`.
482
483 Whether Gazelle should generate build files in the repository. In `"auto"`
484 mode, Gazelle will run if there is no build file in the repository root
485 directory.""",
486 values = [
487 "on",
488 "auto",
489 "off",
490 ],
491 ),
492 "build_naming_convention": attr.string(
493 values = [
494 "go_default_library",
495 "import",
496 "import_alias",
497 ],
498 default = "import_alias",
499 doc = """Sets the library naming convention to use when resolving dependencies against this external
500 repository. If unset, the convention from the external workspace is used.
501 Legal values are `go_default_library`, `import`, and `import_alias`.
502
503 See the `gazelle:go_naming_convention` directive in [Directives] for more information.""",
504 ),
505 "build_tags": attr.string_list(
506 doc = "This sets Gazelle's `-build_tags` command line flag.",
507 ),
508 "build_file_proto_mode": attr.string(
509 doc = """One of `"default"`, `"legacy"`, `"disable"`, `"disable_global"` or `"package"`.
510
511 This sets Gazelle's `-proto` command line flag. See [Directives] for more
512 information on each mode.""",
513 values = [
514 "",
515 "default",
516 "package",
517 "disable",
518 "disable_global",
519 "legacy",
520 ],
521 ),
522 "build_extra_args": attr.string_list(
523 doc = "A list of additional command line arguments to pass to Gazelle when generating build files.",
524 ),
525 "build_config": attr.label(
526 default = "@bazel_gazelle_go_repository_config//:WORKSPACE",
527 doc = """A file that Gazelle should read to learn about external repositories before
528 generating build files. This is useful for dependency resolution. For example,
529 a `go_repository` rule in this file establishes a mapping between a
530 repository name like `golang.org/x/tools` and a workspace name like
531 `org_golang_x_tools`. Workspace directives like
532 `# gazelle:repository_macro` are recognized.
533
534 `go_repository` rules will be re-evaluated when parts of WORKSPACE related
535 to Gazelle's configuration are changed, including Gazelle directives and
536 `go_repository` `name` and `importpath` attributes.
537 Their content should still be fetched from a local cache, but build files
538 will be regenerated. If this is not desirable, `build_config` may be set
539 to a less frequently updated file or `None` to disable this functionality.""",
540 ),
541 "build_directives": attr.string_list(
542 default = [],
543 doc = """A list of directives to be written to the root level build file before
544 Calling Gazelle to generate build files. Each string in the list will be
545 prefixed with `#` automatically. A common use case is to pass a list of
546 Gazelle directives.""",
547 ),
548
549 # Patches to apply after running gazelle.
550 "patches": attr.label_list(
551 doc = "A list of patches to apply to the repository after gazelle runs.",
552 ),
553 "patch_tool": attr.string(
554 default = "",
555 doc = """The patch tool used to apply `patches`. If this is specified, Bazel will
556 use the specifed patch tool instead of the Bazel-native patch implementation.""",
557 ),
558 "patch_args": attr.string_list(
559 default = ["-p0"],
560 doc = "Arguments passed to the patch tool when applying patches.",
561 ),
562 "patch_cmds": attr.string_list(
563 default = [],
564 doc = "Commands to run in the repository after patches are applied.",
565 ),
566
567 # Attributes that affect the verbosity of logging:
568 "debug_mode": attr.bool(
569 default = False,
570 doc = """Enables logging of fetch_repo and Gazelle output during succcesful runs. Gazelle can be noisy
571 so this defaults to `False`. However, setting to `True` can be useful for debugging build failures and
572 unexpected behavior for the given rule.
573 """,
574 ),
575 },
576)
577"""See repository.md#go-repository for full documentation."""
View as plain text