diff --git a/nix/esp-idf/default.nix b/nix/esp-idf/default.nix index d46cba554877c9716c66c273bba15ca6d93051a6..2ccc3521a20b261a69b98352b630f882bdf92177 100644 --- a/nix/esp-idf/default.nix +++ b/nix/esp-idf/default.nix @@ -99,6 +99,18 @@ let pyyaml ]; }; + + pyclang = buildPythonPackage rec { + pname = "pyclang"; + version = "0.2.3"; + src = fetchPypi { + inherit pname version; + sha256 = "sha256-gl3ZaK7/CpMICJlPdPqHfYXb/3MxlKiMpC4AyUtv7MY="; + }; + propagatedBuildInputs = [ + pyyaml + ]; + }; }; in stdenv.mkDerivation rec { @@ -110,7 +122,7 @@ stdenv.mkDerivation rec { # This is so that downstream derivations will have IDF_PATH set. setupHook = ./setup-hook.sh; - propagatedBuildInputs = with python3Packages; [ + propagatedBuildInputs = (with python3Packages; [ setuptools click future pyelftools urllib3 jinja2 itsdangerous pyyaml @@ -155,11 +167,10 @@ stdenv.mkDerivation rec { }; }); }) - - deps.esp-idf-monitor - deps.esp-idf-kconfig - deps.esp-idf-size - ]; + ]) ++ (with deps; [ + esp-idf-monitor esp-idf-kconfig esp-idf-size + pyclang + ]); patches = [ ./rack-off-me-nix-mate.patch diff --git a/nix/esp-llvm.nix b/nix/esp-llvm.nix new file mode 100644 index 0000000000000000000000000000000000000000..e36a43bfc09e1cdb046c59d0d0a97f1ced96b1b9 --- /dev/null +++ b/nix/esp-llvm.nix @@ -0,0 +1,52 @@ +{ version ? "15.0.0-20221201" +, hash ? "sha256-g55a36f0SYLootgoaA9uSqQ13NPR33ZeAvAVsEKGBW8=" +, stdenv +, lib +, fetchurl +, makeWrapper +, buildFHSUserEnv +}: + +let + fhsEnv = buildFHSUserEnv { + name = "esp-llvm-env"; + targetPkgs = pkgs: with pkgs; [ zlib libxml2 ]; + runScript = ""; + }; +in + +assert stdenv.system == "x86_64-linux"; + +stdenv.mkDerivation rec { + pname = "esp-llvm"; + inherit version; + + src = fetchurl { + url = "https://github.com/espressif/llvm-project/releases/download/esp-${version}/llvm-esp-${version}-linux-amd64.tar.xz"; + inherit hash; + }; + + buildInputs = [ makeWrapper ]; + + phases = [ "unpackPhase" "installPhase" ]; + + installPhase = '' + cp -r . $out + for FILE in $(ls $out/bin); do + FILE_PATH="$out/bin/$FILE" + FILE_PATH_UNWRAPPED="$out/.bin-unwrapped/$FILE" + mkdir -p $out/.bin-unwrapped + if [[ -x $FILE_PATH ]]; then + mv $FILE_PATH $FILE_PATH_UNWRAPPED + makeWrapper ${fhsEnv}/bin/esp-llvm-env $FILE_PATH --add-flags "$FILE_PATH_UNWRAPPED" + fi + done + ''; + + meta = with lib; { + description = "Espressif LLVM/Clang fork binary build"; + homepage = "https://docs.espressif.com/projects/esp-idf/en/stable/get-started/linux-setup.html"; + license = licenses.gpl3; + }; +} + diff --git a/nix/run-clang-tidy/default.nix b/nix/run-clang-tidy/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..d60ede0cea933348bab085bd4f107909e4418cfd --- /dev/null +++ b/nix/run-clang-tidy/default.nix @@ -0,0 +1,22 @@ +{ stdenv +, lib +, fetchurl +, makeWrapper +, buildFHSUserEnv +}: + +stdenv.mkDerivation rec { + pname = "run-clang-tidy"; + version = "20230720"; + + src = ./run-clang-tidy.py; + + phases = [ "installPhase" ]; + + installPhase = '' + mkdir -p $out/bin + cp $src $out/bin/run-clang-tidy.py + chmod +x $out/bin/run-clang-tidy.py + ''; +} + diff --git a/nix/run-clang-tidy/run-clang-tidy.py b/nix/run-clang-tidy/run-clang-tidy.py new file mode 100644 index 0000000000000000000000000000000000000000..e0c2d63d2024cf990108c0c564fb3c96721b3aa4 --- /dev/null +++ b/nix/run-clang-tidy/run-clang-tidy.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +# +# ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# +# FIXME: Integrate with clang-tidy-diff.py + + +""" +Parallel clang-tidy runner +========================== + +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. + +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD + +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard + +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy + +Compilation database setup: +http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + +from __future__ import print_function + +import argparse +import glob +import json +import multiprocessing +import os +import queue +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +try: + import yaml +except ImportError: + yaml = None + + +def strtobool(val): + """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" + + val = val.lower() + if val in ["", "true", "1"]: + return True + elif val in ["false", "0"]: + return False + + # Return ArgumentTypeError so that argparse does not substitute its own error message + raise argparse.ArgumentTypeError( + "'{}' is invalid value for boolean argument! Try 0 or 1.".format(val) + ) + + +def find_compilation_database(path): + """Adjusts the directory until a compilation database is found.""" + result = os.path.realpath("./") + while not os.path.isfile(os.path.join(result, path)): + parent = os.path.dirname(result) + if result == parent: + print("Error: could not find compilation database.") + sys.exit(1) + result = parent + return result + + +def make_absolute(f, directory): + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def get_tidy_invocation( + f, + clang_tidy_binary, + checks, + tmpdir, + build_path, + header_filter, + allow_enabling_alpha_checkers, + extra_arg, + extra_arg_before, + quiet, + config_file_path, + config, + line_filter, + use_color, + plugins, + warnings_as_errors, +): + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + if allow_enabling_alpha_checkers: + start.append("-allow-enabling-analyzer-alpha-checkers") + if header_filter is not None: + start.append("-header-filter=" + header_filter) + if line_filter is not None: + start.append("-line-filter=" + line_filter) + if use_color is not None: + if use_color: + start.append("--use-color") + else: + start.append("--use-color=false") + if checks: + start.append("-checks=" + checks) + if tmpdir is not None: + start.append("-export-fixes") + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append("-extra-arg=%s" % arg) + for arg in extra_arg_before: + start.append("-extra-arg-before=%s" % arg) + start.append("-p=" + build_path) + if quiet: + start.append("-quiet") + if config_file_path: + start.append("--config-file=" + config_file_path) + elif config: + start.append("-config=" + config) + for plugin in plugins: + start.append("-load=" + plugin) + if warnings_as_errors: + start.append("--warnings-as-errors=" + warnings_as_errors) + start.append(f) + return start + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): + content = yaml.safe_load(open(replacefile, "r")) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {"MainSourceFile": "", mergekey: merged} + with open(mergefile, "w") as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, "w").close() + + +def find_binary(arg, name, build_path): + """Get the path for a binary or exit""" + if arg: + if shutil.which(arg): + return arg + else: + raise SystemExit( + "error: passed binary '{}' was not found or is not executable".format( + arg + ) + ) + + built_path = os.path.join(build_path, "bin", name) + binary = shutil.which(name) or shutil.which(built_path) + if binary: + return binary + else: + raise SystemExit( + "error: failed to find {} in $PATH or at {}".format(name, built_path) + ) + + +def apply_fixes(args, clang_apply_replacements_binary, tmpdir): + """Calls clang-apply-fixes on a given directory.""" + invocation = [clang_apply_replacements_binary] + invocation.append("-ignore-insert-conflict") + if args.format: + invocation.append("-format") + if args.style: + invocation.append("-style=" + args.style) + invocation.append(tmpdir) + subprocess.call(invocation) + + +def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + name = queue.get() + invocation = get_tidy_invocation( + name, + clang_tidy_binary, + args.checks, + tmpdir, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + ) + + proc = subprocess.Popen( + invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + output, err = proc.communicate() + if proc.returncode != 0: + if proc.returncode < 0: + msg = "%s: terminated by signal %d\n" % (name, -proc.returncode) + err += msg.encode("utf-8") + failed_files.append(name) + with lock: + sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8")) + if len(err) > 0: + sys.stdout.flush() + sys.stderr.write(err.decode("utf-8")) + queue.task_done() + + +def main(): + parser = argparse.ArgumentParser( + description="Runs clang-tidy over all files " + "in a compilation database. Requires " + "clang-tidy and clang-apply-replacements in " + "$PATH or in your build directory." + ) + parser.add_argument( + "-allow-enabling-alpha-checkers", + action="store_true", + help="allow alpha checkers from " "clang-analyzer.", + ) + parser.add_argument( + "-clang-tidy-binary", metavar="PATH", help="path to clang-tidy binary" + ) + parser.add_argument( + "-clang-apply-replacements-binary", + metavar="PATH", + help="path to clang-apply-replacements binary", + ) + parser.add_argument( + "-checks", + default=None, + help="checks filter, when not specified, use clang-tidy " "default", + ) + config_group = parser.add_mutually_exclusive_group() + config_group.add_argument( + "-config", + default=None, + help="Specifies a configuration in YAML/JSON format: " + " -config=\"{Checks: '*', " + ' CheckOptions: {x: y}}" ' + "When the value is empty, clang-tidy will " + "attempt to find a file named .clang-tidy for " + "each source file in its parent directories.", + ) + config_group.add_argument( + "-config-file", + default=None, + help="Specify the path of .clang-tidy or custom config " + "file: e.g. -config-file=/some/path/myTidyConfigFile. " + "This option internally works exactly the same way as " + "-config option after reading specified config file. " + "Use either -config-file or -config, not both.", + ) + parser.add_argument( + "-header-filter", + default=None, + help="regular expression matching the names of the " + "headers to output diagnostics from. Diagnostics from " + "the main file of each translation unit are always " + "displayed.", + ) + parser.add_argument( + "-line-filter", + default=None, + help="List of files with line ranges to filter the" "warnings.", + ) + if yaml: + parser.add_argument( + "-export-fixes", + metavar="filename", + dest="export_fixes", + help="Create a yaml file to store suggested fixes in, " + "which can be applied with clang-apply-replacements.", + ) + parser.add_argument( + "-j", + type=int, + default=0, + help="number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "files", nargs="*", default=[".*"], help="files to be processed (regex on path)" + ) + parser.add_argument("-fix", action="store_true", help="apply fix-its") + parser.add_argument( + "-format", action="store_true", help="Reformat code " "after applying fixes" + ) + parser.add_argument( + "-style", + default="file", + help="The style of reformat " "code after applying fixes", + ) + parser.add_argument( + "-use-color", + type=strtobool, + nargs="?", + const=True, + help="Use colors in diagnostics, overriding clang-tidy's" + " default behavior. This option overrides the 'UseColor" + "' option in .clang-tidy file, if any.", + ) + parser.add_argument( + "-p", dest="build_path", help="Path used to read a compile command database." + ) + parser.add_argument( + "-extra-arg", + dest="extra_arg", + action="append", + default=[], + help="Additional argument to append to the compiler " "command line.", + ) + parser.add_argument( + "-extra-arg-before", + dest="extra_arg_before", + action="append", + default=[], + help="Additional argument to prepend to the compiler " "command line.", + ) + parser.add_argument( + "-quiet", action="store_true", help="Run clang-tidy in quiet mode" + ) + parser.add_argument( + "-load", + dest="plugins", + action="append", + default=[], + help="Load the specified plugin in clang-tidy.", + ) + parser.add_argument( + "-warnings-as-errors", + default=None, + help="Upgrades warnings to errors. Same format as " "'-checks'", + ) + args = parser.parse_args() + + db_path = "compile_commands.json" + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path) + + tmpdir = None + if args.fix: + clang_apply_replacements_binary = find_binary( + args.clang_apply_replacements_binary, "clang-apply-replacements", build_path + ) + tmpdir = tempfile.mkdtemp() + + try: + invocation = get_tidy_invocation( + "", + clang_tidy_binary, + args.checks, + None, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + args.warnings_as_errors, + ) + invocation.append("-list-checks") + invocation.append("-") + if args.quiet: + # Even with -quiet we still want to check if we can call clang-tidy. + with open(os.devnull, "w") as dev_null: + subprocess.check_call(invocation, stdout=dev_null) + else: + subprocess.check_call(invocation) + except: + print("Unable to run clang-tidy.", file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = set( + [make_absolute(entry["file"], entry["directory"]) for entry in database] + ) + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile("|".join(args.files)) + + return_code = 0 + try: + # Spin up a bunch of tidy-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread( + target=run_tidy, + args=( + args, + clang_tidy_binary, + tmpdir, + build_path, + task_queue, + lock, + failed_files, + ), + ) + t.daemon = True + t.start() + + # Fill the queue with files. + for name in files: + if file_name_re.search(name): + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + if len(failed_files): + return_code = 1 + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print("\nCtrl-C detected, goodbye.") + if tmpdir: + shutil.rmtree(tmpdir) + os.kill(0, 9) + + if yaml and args.export_fixes: + print("Writing fixes to " + args.export_fixes + " ...") + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + print("Error exporting fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if args.fix: + print("Applying fixes ...") + try: + apply_fixes(args, clang_apply_replacements_binary, tmpdir) + except: + print("Error applying fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) + + +if __name__ == "__main__": + main() diff --git a/nix/shell.nix b/nix/shell.nix index 6961850919b8a7dfd8d493f245400da3fbff7375..7313b4fe922578038ac54ec8785b095d4125dd04 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -6,6 +6,8 @@ let gcc-xtensa-esp32s3-elf-bin = super.callPackage ./esp32s3-toolchain-bin.nix {}; openocd-esp32-bin = super.callPackage ./openocd-esp32-bin.nix {}; esp-idf = super.callPackage ./esp-idf {}; + esp-llvm = super.callPackage ./esp-llvm.nix {}; + run-clang-tidy = super.callPackage ./run-clang-tidy {}; }) ]; }; @@ -16,7 +18,9 @@ in with nixpkgs; pkgs.mkShell { gcc-xtensa-esp32s3-elf-bin openocd-esp32-bin esp-idf + esp-llvm esptool + run-clang-tidy git wget gnumake flex bison gperf pkgconfig