Skip to content

Commit 15fbac4

Browse files
authored
Rewrite Buildifier support (bazelbuild#1129)
There have been many problems with Buildifier on Bazel CI, mostly stemming from the fact that Buildifier has evolved a lot, but our CI support hasn't kept up. Ideally there would have been many smaller commits over time, but well... This commit implements the following changes: 1. The code now uses a single Buildifier invocation for linting and format checks. 2. We now call Buildifier with --format=json, thus removing the need for error-prone parsing of unstructured text. 3. The old code checked stderr for Buildifier warnings, but at some point Buildifier started printing them to stdout only. 4. Finally we no longer determine the list of source files that should be passed to Buildifier, but simply use Buildifier's recursive mode ("-r ."). Progress towards bazelbuild#1080
1 parent 5569c11 commit 15fbac4

File tree

1 file changed

+98
-84
lines changed

1 file changed

+98
-84
lines changed

Diff for: buildifier/buildifier.py

+98-84
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@
1616
from distutils.version import LooseVersion
1717
from urllib.request import urlopen
1818

19-
regex = re.compile(
20-
r"^(?P<filename>[^:]*):(?P<line>\d*):(?:(?P<column>\d*):)? (?P<message_id>[^:]*): (?P<message>.*?) \((?P<message_url>.*?)\)$",
21-
re.MULTILINE | re.DOTALL
19+
BUILDIFIER_VERSION_PATTERN = re.compile(
20+
r"^buildifier version: ([\.\w]+)$", re.MULTILINE
2221
)
2322

24-
BUILDIFIER_VERSION_PATTERN = re.compile(r"^buildifier version: ([\.\w]+)$", re.MULTILINE)
25-
2623
# https://github.com/bazelbuild/buildtools/blob/master/buildifier/buildifier.go#L333
2724
# Buildifier error code for "needs formatting". We should fail on all other error codes > 0
2825
# since they indicate a problem in how Buildifier is used.
@@ -34,7 +31,9 @@
3431

3532
BUILDIFIER_RELEASES_URL = "https://api.github.com/repos/bazelbuild/buildtools/releases"
3633

37-
BUILDIFIER_DEFAULT_DISPLAY_URL = "https://github.com/bazelbuild/buildtools/tree/master/buildifier"
34+
BUILDIFIER_DEFAULT_DISPLAY_URL = (
35+
"https://github.com/bazelbuild/buildtools/tree/master/buildifier"
36+
)
3837

3938

4039
def eprint(*args, **kwargs):
@@ -51,7 +50,14 @@ def upload_output(output):
5150

5251
eprint("+++ :buildkite: Uploading output via 'buildkite annotate'")
5352
result = subprocess.run(
54-
["buildkite-agent", "annotate", "--style", "warning", "--context", "buildifier"],
53+
[
54+
"buildkite-agent",
55+
"annotate",
56+
"--style",
57+
"warning",
58+
"--context",
59+
"buildifier",
60+
],
5561
input=output.encode(locale.getpreferredencoding(False)),
5662
)
5763
if result.returncode != 0:
@@ -66,14 +72,18 @@ def print_error(failing_task, message):
6672
output = "##### :bazel: buildifier: error while {}:\n".format(failing_task)
6773
output += "<pre><code>{}</code></pre>".format(html.escape(message))
6874
if "BUILDKITE_JOB_ID" in os.environ:
69-
output += "\n\nSee [job {job}](#{job})\n".format(job=os.environ["BUILDKITE_JOB_ID"])
75+
output += "\n\nSee [job {job}](#{job})\n".format(
76+
job=os.environ["BUILDKITE_JOB_ID"]
77+
)
7078

7179
upload_output(output)
7280

7381

7482
def get_file_url(filename, line):
7583
commit = os.environ.get("BUILDKITE_COMMIT")
76-
repo = os.environ.get("BUILDKITE_PULL_REQUEST_REPO", os.environ.get("BUILDKITE_REPO", None))
84+
repo = os.environ.get(
85+
"BUILDKITE_PULL_REQUEST_REPO", os.environ.get("BUILDKITE_REPO", None)
86+
)
7787
if not commit or not repo:
7888
return None
7989

@@ -89,7 +99,7 @@ def get_file_url(filename, line):
8999
return None
90100

91101

92-
def run_buildifier(binary, flags, files=None, version=None, what=None):
102+
def run_buildifier(binary, flags, version=None, what=None):
93103
label = "+++ :bazel: Running "
94104
if version:
95105
label += "Buildifier " + version
@@ -100,11 +110,9 @@ def run_buildifier(binary, flags, files=None, version=None, what=None):
100110

101111
eprint(label)
102112

103-
args = [binary] + flags
104-
if files:
105-
args += files
106-
107-
return subprocess.run(args, capture_output=True, universal_newlines=True)
113+
return subprocess.run(
114+
[binary] + flags, capture_output=True, universal_newlines=True
115+
)
108116

109117

110118
def create_heading(issue_type, issue_count):
@@ -134,15 +142,23 @@ def get_releases():
134142
body = res.read()
135143
content = body.decode(res.info().get_content_charset("iso-8859-1"))
136144

137-
return {r["tag_name"]: get_release_urls(r) for r in json.loads(content) if not r["prerelease"]}
145+
return {
146+
r["tag_name"]: get_release_urls(r)
147+
for r in json.loads(content)
148+
if not r["prerelease"]
149+
}
138150

139151

140152
def get_release_urls(release):
141153
buildifier_assets = [
142-
a for a in release["assets"] if a["name"] in ("buildifier", "buildifier-linux-amd64")
154+
a
155+
for a in release["assets"]
156+
if a["name"] in ("buildifier", "buildifier-linux-amd64")
143157
]
144158
if not buildifier_assets:
145-
raise Exception("There is no Buildifier binary for release {}".format(release["tag_name"]))
159+
raise Exception(
160+
"There is no Buildifier binary for release {}".format(release["tag_name"])
161+
)
146162

147163
return release["html_url"], buildifier_assets[0]["browser_download_url"]
148164

@@ -156,6 +172,31 @@ def download_buildifier(url):
156172
return path
157173

158174

175+
def format_lint_warning(filename, warning):
176+
line_number = warning["start"]["line"]
177+
link_start, link_end, column_text = "", "", ""
178+
179+
file_url = get_file_url(filename, line_number)
180+
if file_url:
181+
link_start = '<a href="{}">'.format(file_url)
182+
link_end = "</a>"
183+
184+
column = warning["start"].get("column")
185+
if column:
186+
column_text = ":{}".format(column)
187+
188+
return '{link_start}{filename}:{line}{column}{link_end}: <a href="{help_url}">{category}</a>: {message}'.format(
189+
link_start=link_start,
190+
filename=filename,
191+
line=line_number,
192+
column=column_text,
193+
link_end=link_end,
194+
help_url=warning["url"],
195+
category=warning["category"],
196+
message=warning["message"],
197+
)
198+
199+
159200
def main(argv=None):
160201
if argv is None:
161202
argv = sys.argv[1:]
@@ -173,102 +214,75 @@ def main(argv=None):
173214
print_error("downloading Buildifier", str(ex))
174215
return 1
175216

176-
# Gather all files to process.
177-
eprint("+++ :female-detective: Looking for WORKSPACE, BUILD, BUILD.bazel and *.bzl files")
178-
files = []
179-
build_bazel_found = False
180-
for root, _, filenames in os.walk("."):
181-
for filename in filenames:
182-
if fnmatch.fnmatch(filename, "BUILD.bazel"):
183-
build_bazel_found = True
184-
for pattern in ("WORKSPACE", "BUILD", "BUILD.bazel", "*.bzl"):
185-
if fnmatch.fnmatch(filename, pattern):
186-
files.append(os.path.relpath(os.path.join(root, filename)))
187-
if build_bazel_found:
188-
eprint(
189-
"Found BUILD.bazel files in the workspace, thus ignoring BUILD files without suffix."
190-
)
191-
files = [fname for fname in files if not fnmatch.fnmatch(os.path.basename(fname), "BUILD")]
192-
if not files:
193-
eprint("No files found, exiting.")
194-
return 0
195-
196-
files = sorted(files)
197-
198217
# Determine Buildifier version if the user did not request a specific version.
199218
if not version:
200219
eprint("+++ :female-detective: Detecting Buildifier version")
201-
version_result = run_buildifier(buildifier_binary, ["--version"], what="Version info")
220+
version_result = run_buildifier(
221+
buildifier_binary, ["--version"], what="Version info"
222+
)
202223
match = BUILDIFIER_VERSION_PATTERN.search(version_result.stdout)
203224
version = match.group(1) if match and match.group(1) != "redacted" else None
204225

205-
# Run formatter before linter since --lint=warn implies --mode=fix,
206-
# thus fixing any format issues.
207-
formatter_result = run_buildifier(
208-
buildifier_binary, ["--mode=check"], files=files, version=version, what="Format check"
209-
)
210-
if formatter_result.returncode and formatter_result.returncode != BUILDIFIER_FORMAT_ERROR_CODE:
211-
print_error("checking format", formatter_result.stderr)
212-
return formatter_result.returncode
213-
214-
# Format: "<file name> # reformated"
215-
unformatted_files = [l.partition(" ")[0] for l in formatter_result.stderr.splitlines()]
216-
if unformatted_files:
217-
eprint(
218-
"+++ :construction: Found {} file(s) that must be formatted".format(
219-
len(unformatted_files)
220-
)
221-
)
222-
223-
lint_flags = ["--lint=warn"]
226+
flags = ["--mode=check", "--lint=warn"]
224227
warnings = os.getenv(WARNINGS_ENV_VAR)
225228
if warnings:
226229
eprint("Running Buildifier with the following warnings: {}".format(warnings))
227-
lint_flags.append("--warnings={}".format(warnings))
230+
flags.append("--warnings={}".format(warnings))
228231

229-
linter_result = run_buildifier(
230-
buildifier_binary, lint_flags, files=files, version=version, what="Lint checks"
232+
result = run_buildifier(
233+
buildifier_binary,
234+
flags + ["--format=json", "-r", "."],
235+
version=version,
236+
what="Format & lint checks",
231237
)
232-
if linter_result.returncode == 0 and not unformatted_files:
238+
239+
if result.returncode and result.returncode != BUILDIFIER_FORMAT_ERROR_CODE:
240+
print_error("Buildifier failed", result.stderr)
241+
return result.returncode
242+
243+
data = json.loads(result.stdout)
244+
if data["success"]:
233245
# If buildifier was happy, there's nothing left to do for us.
234246
eprint("+++ :tada: Buildifier found nothing to complain about")
235247
return 0
236248

249+
unformatted_files = []
250+
lint_findings = []
251+
for file in data["files"]:
252+
filename = file["filename"]
253+
254+
if not file["formatted"]:
255+
unformatted_files.append(filename)
256+
257+
for warning in file["warnings"]:
258+
lint_findings.append(format_lint_warning(filename, warning))
259+
237260
output = ""
238261
if unformatted_files:
262+
eprint(
263+
"+++ :construction: Found {} file(s) that must be formatted".format(
264+
len(unformatted_files)
265+
)
266+
)
239267
output = create_heading("format", len(unformatted_files))
240268
display_version = " {}".format(version) if version else ""
241269
output += (
242-
"Please download <a href=\"{}\">buildifier{}</a> and run the following "
270+
'Please download <a href="{}">buildifier{}</a> and run the following '
243271
"command in your workspace:<br/><pre><code>buildifier {}</code></pre>"
244272
"\n".format(display_url, display_version, " ".join(unformatted_files))
245273
)
246274

247-
# Parse output.
248-
if linter_result.returncode:
249-
eprint("+++ :gear: Parsing buildifier output")
250-
findings = list(regex.finditer(linter_result.stderr))
251-
output += create_heading("lint", len(findings))
275+
if lint_findings:
276+
eprint("+++ :gear: Rendering lint warnings")
277+
output += create_heading("lint", len(lint_findings))
252278
output += "<pre><code>"
253-
for finding in findings:
254-
file_url = get_file_url(finding["filename"], finding["line"])
255-
if file_url:
256-
output += '<a href="{}">{}:{}</a>:'.format(
257-
file_url, finding["filename"], finding["line"]
258-
)
259-
else:
260-
output += "{}:{}:".format(finding["filename"], finding["line"])
261-
if finding["column"]:
262-
output += "{}:".format(finding["column"])
263-
output += ' <a href="{}">{}</a>: {}\n'.format(
264-
finding["message_url"], finding["message_id"], finding["message"]
265-
)
279+
output += "\n".join(lint_findings)
266280
output = output.strip() + "</pre></code>"
267281

268282
upload_output(output)
269283

270284
# Preserve buildifier's exit code.
271-
return max(linter_result.returncode, formatter_result.returncode)
285+
return BUILDIFIER_FORMAT_ERROR_CODE
272286

273287

274288
if __name__ == "__main__":

0 commit comments

Comments
 (0)