1# Copyright 2023 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
15visibility([
16 "//tests/bzlmod/...",
17])
18
19def deps_from_go_mod(module_ctx, go_mod_label):
20 """Loads the entries from a go.mod file.
21
22 Args:
23 module_ctx: a https://bazel.build/rules/lib/module_ctx object passed
24 from the MODULE.bazel call.
25 go_mod_label: a Label for a `go.mod` file.
26
27 Returns:
28 a tuple (Go module path, deps, replace map), where deps is a list of structs representing
29 `require` statements from the go.mod file.
30 """
31 _check_go_mod_name(go_mod_label.name)
32
33 go_mod_path = module_ctx.path(go_mod_label)
34 go_mod_content = module_ctx.read(go_mod_path)
35 go_mod = parse_go_mod(go_mod_content, go_mod_path)
36
37 if go_mod.go[0] != 1 or go_mod.go[1] < 17:
38 # go.mod files only include entries for all transitive dependencies as
39 # of Go 1.17.
40 fail("go_deps.from_file requires a go.mod file generated by Go 1.17 or later. Fix {} with 'go mod tidy -go=1.17'.".format(go_mod_label))
41
42 deps = []
43 for require in go_mod.require:
44 deps.append(struct(
45 path = require.path,
46 version = require.version,
47 indirect = require.indirect,
48 ))
49
50 return go_mod.module, deps, go_mod.replace_map
51
52def parse_go_mod(content, path):
53 # See https://go.dev/ref/mod#go-mod-file.
54
55 # Valid directive values understood by this parser never contain tabs or
56 # carriage returns, so we can simplify the parsing below by canonicalizing
57 # whitespace upfront.
58 content = content.replace("\t", " ").replace("\r", " ")
59
60 state = {
61 "module": None,
62 "go": None,
63 "require": [],
64 "replace": {},
65 }
66
67 current_directive = None
68 for line_no, line in enumerate(content.splitlines(), 1):
69 tokens, comment = _tokenize_line(line, path, line_no)
70 if not tokens:
71 continue
72
73 if not current_directive:
74 if tokens[0] not in ["module", "go", "require", "replace", "exclude", "retract", "toolchain"]:
75 fail("{}:{}: unexpected token '{}' at start of line".format(path, line_no, tokens[0]))
76 if len(tokens) == 1:
77 fail("{}:{}: expected another token after '{}'".format(path, line_no, tokens[0]))
78
79 # The 'go' directive only has a single-line form and is thus parsed
80 # here rather than in _parse_directive.
81 if tokens[0] == "go":
82 if len(tokens) == 1:
83 fail("{}:{}: expected another token after 'go'".format(path, line_no))
84 if state["go"] != None:
85 fail("{}:{}: unexpected second 'go' directive".format(path, line_no))
86 state["go"] = tokens[1]
87 if len(tokens) > 2:
88 fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[2], tokens[1]))
89
90 if tokens[1] == "(":
91 current_directive = tokens[0]
92 if len(tokens) > 2:
93 fail("{}:{}: unexpected token '{}' after '('".format(path, line_no, tokens[2]))
94 continue
95
96 _parse_directive(state, tokens[0], tokens[1:], comment, path, line_no)
97
98 elif tokens[0] == ")":
99 current_directive = None
100 if len(tokens) > 1:
101 fail("{}:{}: unexpected token '{}' after ')'".format(path, line_no, tokens[1]))
102 continue
103
104 else:
105 _parse_directive(state, current_directive, tokens, comment, path, line_no)
106
107 module = state["module"]
108 if not module:
109 fail("Expected a module directive in go.mod file")
110
111 go = state["go"]
112 if not go:
113 # "As of the Go 1.17 release, if the go directive is missing, go 1.16 is assumed."
114 go = "1.16"
115
116 # The go directive can contain patch and pre-release versions, but we omit them.
117 major, minor = go.split(".")[:2]
118
119 return struct(
120 module = module,
121 go = (int(major), int(minor)),
122 require = tuple(state["require"]),
123 replace_map = state["replace"],
124 )
125
126def _parse_directive(state, directive, tokens, comment, path, line_no):
127 if directive == "module":
128 if state["module"] != None:
129 fail("{}:{}: unexpected second 'module' directive".format(path, line_no))
130 if len(tokens) > 1:
131 fail("{}:{}: unexpected token '{}' after '{}'".format(path, line_no, tokens[1]))
132 state["module"] = tokens[0]
133 elif directive == "require":
134 if len(tokens) != 2:
135 fail("{}:{}: expected module path and version in 'require' directive".format(path, line_no))
136 state["require"].append(struct(
137 path = tokens[0],
138 version = tokens[1],
139 indirect = comment == "indirect",
140 ))
141 elif directive == "replace":
142 # A replace directive might use a local file path beginning with ./ or ../
143 # These are not supported with gazelle~go_deps.
144 if (len(tokens) == 3 and tokens[2][0] == ".") or (len(tokens) > 3 and tokens[3][0] == "."):
145 fail("{}:{}: local file path not supported in replace directive: '{}'".format(path, line_no, tokens[2]))
146
147 # replacements key off of the from_path
148 from_path = tokens[0]
149
150 # pattern: replace from_path => to_path to_version
151 if len(tokens) == 4 and tokens[1] == "=>":
152 state["replace"][from_path] = struct(
153 from_version = None,
154 to_path = tokens[2],
155 version = _canonicalize_raw_version(tokens[3]),
156 )
157 # pattern: replace from_path from_version => to_path to_version
158 elif len(tokens) == 5 and tokens[2] == "=>":
159 state["replace"][from_path] = struct(
160 from_version = _canonicalize_raw_version(tokens[1]),
161 to_path = tokens[3],
162 version = _canonicalize_raw_version(tokens[4]),
163 )
164 else:
165 fail(
166 "{}:{}: replace directive must follow pattern: ".format(path, line_no) +
167 "'replace from_path from_version => to_path to_version' or " +
168 "'replace from_path => to_path to_version'"
169 )
170
171 # TODO: Handle exclude.
172
173def _tokenize_line(line, path, line_no):
174 tokens = []
175 r = line
176 for _ in range(len(line)):
177 r = r.strip()
178 if not r:
179 break
180
181 if r[0] == "`":
182 end = r.find("`", 1)
183 if end == -1:
184 fail("{}:{}: unterminated raw string".format(path, line_no))
185
186 tokens.append(r[1:end])
187 r = r[end + 1:]
188
189 elif r[0] == "\"":
190 value = ""
191 escaped = False
192 found_end = False
193 for pos in range(1, len(r)):
194 c = r[pos]
195
196 if escaped:
197 value += c
198 escaped = False
199 continue
200
201 if c == "\\":
202 escaped = True
203 continue
204
205 if c == "\"":
206 found_end = True
207 break
208
209 value += c
210
211 if not found_end:
212 fail("{}:{}: unterminated interpreted string".format(path, line_no))
213
214 tokens.append(value)
215 r = r[pos + 1:]
216
217 elif r.startswith("//"):
218 # A comment always ends the current line
219 return tokens, r[len("//"):].strip()
220
221 else:
222 token, _, r = r.partition(" ")
223 tokens.append(token)
224
225 return tokens, None
226
227def sums_from_go_mod(module_ctx, go_mod_label):
228 """Loads the entries from a go.sum file given a go.mod Label.
229
230 Args:
231 module_ctx: a https://bazel.build/rules/lib/module_ctx object
232 passed from the MODULE.bazel call.
233 go_mod_label: a Label for a `go.mod` file. This label is used
234 to find the associated `go.sum` file.
235
236 Returns:
237 A Dict[(string, string) -> (string)] is retruned where each entry
238 is defined by a Go Module's sum:
239 (path, version) -> (sum)
240 """
241 _check_go_mod_name(go_mod_label.name)
242
243 # We go through a Label so that the module extension is restarted if go.sum
244 # changes. We have to use a canonical label as we may not have visibility
245 # into the module that provides the go.sum.
246 go_sum_label = Label("@@{}//{}:{}".format(
247 go_mod_label.workspace_name,
248 go_mod_label.package,
249 "go.sum",
250 ))
251 go_sum_content = module_ctx.read(go_sum_label)
252 return parse_go_sum(go_sum_content)
253
254def parse_go_sum(content):
255 hashes = {}
256 for line in content.splitlines():
257 path, version, sum = line.split(" ")
258 version = _canonicalize_raw_version(version)
259 if not version.endswith("/go.mod"):
260 hashes[(path, version)] = sum
261 return hashes
262
263def _check_go_mod_name(name):
264 if name != "go.mod":
265 fail("go_deps.from_file requires a 'go.mod' file, not '{}'".format(name))
266
267def _canonicalize_raw_version(raw_version):
268 if raw_version.startswith("v"):
269 return raw_version[1:]
270 return raw_version
View as plain text