| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| def black(ctx): |
| tools_dir = _install_tools(ctx) |
| |
| py_files = [ |
| f |
| for f in ctx.scm.affected_files() |
| if f.endswith(".py") and |
| # recipes.py is vendored from the recipe engine and should be |
| # ignored. |
| f != "recipes.py" |
| ] |
| if not py_files: |
| return |
| |
| original_contents = {} |
| procs = {} |
| for filepath in py_files: |
| original = str(ctx.io.read_file(filepath)) |
| original_contents[filepath] = original |
| procs[filepath] = ctx.os.exec([tools_dir + "/black", "-"], stdin = original) |
| |
| for filepath, proc in procs.items(): |
| res = proc.wait() |
| original = original_contents[filepath] |
| if res.stdout != original: |
| ctx.emit.finding( |
| filepath = filepath, |
| level = "error", |
| replacements = [res.stdout], |
| ) |
| |
| def proto_format(ctx): |
| tools_dir = _install_tools(ctx) |
| |
| procs = [] |
| for p in ctx.scm.affected_files(): |
| if not p.endswith(".proto"): |
| continue |
| |
| # TODO(olivernewman): Use a single `buf format` invocation and the |
| # --diff option once shac knows how to parse diffs. |
| cmd = [ |
| tools_dir + "/buf", |
| "format", |
| "--exit-code", |
| p, |
| ] |
| procs.append((p, ctx.os.exec(cmd, ok_retcodes = [0, 100]))) |
| |
| for p, proc in procs: |
| res = proc.wait() |
| if res.retcode == 100: |
| ctx.emit.finding( |
| filepath = p, |
| level = "error", |
| replacements = [res.stdout], |
| ) |
| |
| def proto_field_numbering(ctx): |
| """Checks that recipe property protobuf files have clean field numbers. |
| |
| Recipe property protos need not subscribe to normal protobuf maintenance |
| conventions such as never deleting fields or changing field numbers, because |
| they are only used to de/serialize JSON-encoded protos, never binary-encoded |
| protos. |
| |
| So it's safe (and required, for code cleanliness) to keep proto fields |
| monotonically increasing with no jumps. |
| |
| Args: |
| ctx: A ctx instance. |
| """ |
| for f in ctx.scm.affected_files(): |
| # The recipe_proto directory contains protos that may be used in other |
| # places than just recipe properties, so we cannot safely renumber their |
| # fields. |
| if not f.endswith(".proto") or f.startswith("recipe_proto/"): |
| continue |
| res = ctx.os.exec( |
| [ |
| "python3", |
| "scripts/renumber_proto_fields.py", |
| "--dry-run", |
| f, |
| ], |
| ).wait() |
| if res.stdout != str(ctx.io.read_file(f)): |
| ctx.emit.finding( |
| filepath = f, |
| level = "error", |
| replacements = [res.stdout], |
| ) |
| |
| def _install_tools(ctx): |
| install_dir = ctx.scm.root + "/.tools" |
| ctx.os.exec( |
| ["scripts/install-shac-tools.sh", install_dir], |
| allow_network = True, |
| ).wait() |
| return install_dir |
| |
| def check_deps(ctx): |
| res = ctx.os.exec( |
| [ |
| "python3", |
| "scripts/cleanup_deps.py", |
| "--check", |
| "--json-output", |
| "-", |
| ], |
| ok_retcodes = [0, 65], |
| ).wait() |
| if res.retcode == 65: |
| for file in json.decode(res.stdout): |
| # TODO(olivernewman): Parse the diff so fixes can be applied with |
| # `shac fix`. |
| ctx.emit.finding( |
| filepath = file, |
| message = "DEPS are malformatted. Run ./scripts/cleanup_deps.py to fix.", |
| level = "error", |
| ) |
| |
| def recipe_style_guide(ctx): |
| """Enforces http://go/fuchsia-recipe-docs#style-guide.""" |
| procs = [] |
| for f in ctx.scm.affected_files(): |
| if f.endswith(".json") and ".expected" in f: |
| test_name = f.split("/")[-1].rsplit(".", 1)[0] |
| if " " in test_name: |
| # It would be nicer to parse the recipe file to find test case |
| # names because then we could emit the finding at the place |
| # where the test name is defined. But because tests are |
| # generated by arbitrary Python code, it's practically |
| # impossible to parse out their names in a foolproof way. |
| ctx.emit.finding( |
| filepath = f, |
| message = "Test name %r should not contain spaces" % test_name, |
| level = "error", |
| ) |
| |
| if not f.endswith(".py"): |
| continue |
| procs.append(ctx.os.exec(["python3", "scripts/enforce_style_guide.py", f])) |
| |
| for proc in procs: |
| res = proc.wait() |
| for finding in json.decode(res.stdout): |
| ctx.emit.finding(**finding) |
| |
| shac.register_check(shac.check(black, formatter = True)) |
| shac.register_check(shac.check(proto_format, formatter = True)) |
| shac.register_check(shac.check(proto_field_numbering, formatter = True)) |
| |
| shac.register_check(check_deps) |
| shac.register_check(recipe_style_guide) |