From 04cac2cc626a42440c962bd83e4bcd69eaa4b3a4 Mon Sep 17 00:00:00 2001 From: Chaitanya Tata Date: Fri, 8 Sep 2023 01:42:59 +0530 Subject: [PATCH] [nrf noup] ci: Add compliance test Use standard Zephyr's compliance in CI, copy the dependencies from Zephyr repo. Signed-off-by: Chaitanya Tata --- .checkpatch.conf | 32 ++++ .github/workflows/compliance.yml | 112 +++++++++++ .gitlint | 60 ++++++ scripts/ci/codeowners.py | 253 +++++++++++++++++++++++++ scripts/ci/requirements.txt | 3 + scripts/gitlint/ncs.py | 59 ++++++ scripts/gitlint/zephyr_commit_rules.py | 132 +++++++++++++ scripts/requirements-fixed.txt | 115 +++++++++++ 8 files changed, 766 insertions(+) create mode 100644 .checkpatch.conf create mode 100644 .github/workflows/compliance.yml create mode 100644 .gitlint create mode 100755 scripts/ci/codeowners.py create mode 100644 scripts/ci/requirements.txt create mode 100644 scripts/gitlint/ncs.py create mode 100644 scripts/gitlint/zephyr_commit_rules.py create mode 100644 scripts/requirements-fixed.txt diff --git a/.checkpatch.conf b/.checkpatch.conf new file mode 100644 index 000000000..9715bdd2a --- /dev/null +++ b/.checkpatch.conf @@ -0,0 +1,32 @@ +--emacs +--summary-file +--show-types +--max-line-length=100 +--min-conf-desc-length=1 + +--ignore BRACES +--ignore PRINTK_WITHOUT_KERN_LEVEL +--ignore SPLIT_STRING +--ignore VOLATILE +--ignore CONFIG_EXPERIMENTAL +--ignore PREFER_KERNEL_TYPES +--ignore PREFER_SECTION +--ignore AVOID_EXTERNS +--ignore NETWORKING_BLOCK_COMMENT_STYLE +--ignore DATE_TIME +--ignore MINMAX +--ignore CONST_STRUCT +--ignore FILE_PATH_CHANGES +--ignore SPDX_LICENSE_TAG +--ignore C99_COMMENT_TOLERANCE +--ignore REPEATED_WORD +--ignore UNDOCUMENTED_DT_STRING +--ignore DT_SPLIT_BINDING_PATCH +--ignore DT_SCHEMA_BINDING_PATCH +--ignore TRAILING_SEMICOLON +--ignore COMPLEX_MACRO +--ignore MULTISTATEMENT_MACRO_USE_DO_WHILE +--ignore ENOSYS +--ignore IS_ENABLED_CONFIG +--ignore EMBEDDED_FUNCTION_NAME +--ignore MACRO_WITH_FLOW_CONTROL diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml new file mode 100644 index 000000000..2b2d5f929 --- /dev/null +++ b/.github/workflows/compliance.yml @@ -0,0 +1,112 @@ +name: Compliance Checks + +on: pull_request + +jobs: + check_compliance: + runs-on: ubuntu-22.04 + name: Run compliance checks on patch series (PR) + steps: + - name: Update PATH for west + run: | + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Checkout the code + uses: actions/checkout@v3 + with: + path: sdk-hostap + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: cache-pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-doc-pip + + - name: Install python dependencies + working-directory: sdk-hostap + run: | + pip3 install setuptools + pip3 install wheel + pip3 install python-magic lxml junitparser gitlint pylint pykwalify yamllint + pip3 install west + + - name: Clone Zephyr downstream + env: + BASE_REF: ${{ github.base_ref }} + working-directory: sdk-hostap + run: | + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + git remote -v + # Ensure there's no merge commits in the PR + #[[ "$(git rev-list --merges --count origin/${BASE_REF}..)" == "0" ]] || \ + #(echo "::error ::Merge commits not allowed, rebase instead";false) + # Sauce tag checks before rebasing + git rev-list --first-parent origin/${BASE_REF}..HEAD | tr '\n' ',' | \ + xargs gitlint -c ncs-sauce-tags.enable=true \ + -c title-starts-with-subsystem.regex=".*" --commits + git rebase origin/${BASE_REF} + # debug + git log --pretty=oneline | head -n 10 + # Clone downstream Zephyr (no west needed as we only need the scripts) + git clone https://github.com/nrfconnect/sdk-zephyr + + - name: Run CODEOWNERS test + id: codeowners + env: + BASE_REF: ${{ github.base_ref }} + working-directory: sdk-hostap + if: contains(github.event.pull_request.user.login, 'dependabot[bot]') != true + run: | + ./scripts/ci/codeowners.py -c origin/${BASE_REF}.. + + - name: Run Compliance Tests + continue-on-error: true + id: compliance + env: + BASE_REF: ${{ github.base_ref }} + working-directory: sdk-hostap + if: contains(github.event.pull_request.user.login, 'dependabot[bot]') != true + run: | + export ZEPHYR_BASE="$(dirname "$(pwd)")/sdk-hostap/sdk-zephyr" + # debug + ls -la + git log --pretty=oneline | head -n 10 + ./scripts/ci/check_compliance.py --annotate -e KconfigBasic -e Kconfig \ + -c origin/${BASE_REF}.. + + - name: upload-results + uses: actions/upload-artifact@v3 + continue-on-error: true + if: contains(github.event.pull_request.user.login, 'dependabot[bot]') != true + with: + name: compliance.xml + path: sdk-hostap/compliance.xml + + - name: check-warns + working-directory: sdk-hostap + if: contains(github.event.pull_request.user.login, 'dependabot[bot]') != true + run: | + export ZEPHYR_BASE="$(dirname "$(pwd)")/sdk-hostap/sdk-zephyr" + if [[ ! -s "compliance.xml" ]]; then + exit 1; + fi + + files=($($ZEPHYR_BASE/scripts/ci/check_compliance.py -l)) + for file in "${files[@]}"; do + f="${file}.txt" + if [[ -s $f ]]; then + errors=$(cat $f) + errors="${errors//'%'/'%25'}" + errors="${errors//$'\n'/'%0A'}" + errors="${errors//$'\r'/'%0D'}" + echo "::error file=${f}::$errors" + exit=1 + fi + done + + if [ "${exit}" == "1" ]; then + exit 1; + fi diff --git a/.gitlint b/.gitlint new file mode 100644 index 000000000..8a33f140b --- /dev/null +++ b/.gitlint @@ -0,0 +1,60 @@ +# All these sections are optional, edit this file as you like. +[general] +ignore=title-trailing-punctuation, T3, title-max-length, T1, body-hard-tab, B3, B1 +# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this +verbosity = 3 +# By default gitlint will ignore merge commits. Set to 'false' to disable. +ignore-merge-commits=false +ignore-revert-commits=false +ignore-fixup-commits=false +ignore-squash-commits=false +# Enable debug mode (prints more output). Disabled by default +debug = false + +# Set the extra-path where gitlint will search for user defined rules +# See http://jorisroovers.github.io/gitlint/user_defined_rules for details +extra-path=scripts/gitlint + +[title-max-length-no-revert] +line-length=120 + +[body-min-line-count] +min-line-count=1 + +[body-max-line-count] +max-line-count=200 + +[title-starts-with-subsystem] +regex = ^(?!subsys:)(([^:]+):)(\s([^:]+):)*\s(.+)$ + +[title-must-not-contain-word] +# Comma-separated list of words that should not occur in the title. Matching is case +# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING" +# will not cause a violation, but "WIP: my title" will. +words=wip + +[title-match-regex] +# python like regex (https://docs.python.org/2/library/re.html) that the +# commit-msg title must be matched to. +# Note that the regex can contradict with other rules if not used correctly +# (e.g. title-must-not-contain-word). +#regex=^US[0-9]* + +[max-line-length-with-exceptions] +# B1 = body-max-line-length +line-length=120 + +[body-min-length] +min-length=3 + +[body-is-missing] +# Whether to ignore this rule on merge commits (which typically only have a title) +# default = True +ignore-merge-commits=false + +[body-changed-file-mention] +# List of files that need to be explicitly mentioned in the body when they are changed +# This is useful for when developers often erroneously edit certain files or git submodules. +# By specifying this rule, developers can only change the file when they explicitly reference +# it in the commit message. +#files=gitlint/rules.py,README.md diff --git a/scripts/ci/codeowners.py b/scripts/ci/codeowners.py new file mode 100755 index 000000000..a8729d4fd --- /dev/null +++ b/scripts/ci/codeowners.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 + +# Copyright (c) 2018,2020 Intel Corporation +# Copyright (c) 2022 Nordic Semiconductor ASA + +import argparse +import collections +import logging +import os +from pathlib import Path +import re +import subprocess +import sys +import shlex + +logger = None + +failures = 0 + +def err(msg): + cmd = sys.argv[0] # Empty if missing + if cmd: + cmd += ": " + sys.exit(f"{cmd}fatal error: {msg}") + + +def cmd2str(cmd): + # Formats the command-line arguments in the iterable 'cmd' into a string, + # for error messages and the like + + return " ".join(shlex.quote(word) for word in cmd) + + +def annotate(severity, file, title, message, line=None, col=None): + """ + https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#about-workflow-commands + """ + notice = f'::{severity} file={file}' + \ + (f',line={line}' if line else '') + \ + (f',col={col}' if col else '') + \ + f',title={title}::{message}' + print(notice) + + +def failure(msg, file='CODEOWNERS', line=None): + global failures + failures += 1 + annotate('error', file=file, title="CODEOWNERS", message=msg, + line=line) + + +def git(*args, cwd=None): + # Helper for running a Git command. Returns the rstrip()ed stdout output. + # Called like git("diff"). Exits with SystemError (raised by sys.exit()) on + # errors. 'cwd' is the working directory to use (default: current + # directory). + + git_cmd = ("git",) + args + try: + cp = subprocess.run(git_cmd, capture_output=True, cwd=cwd) + except OSError as e: + err(f"failed to run '{cmd2str(git_cmd)}': {e}") + + if cp.returncode or cp.stderr: + err(f"'{cmd2str(git_cmd)}' exited with status {cp.returncode} and/or " + f"wrote to stderr.\n" + f"==stdout==\n" + f"{cp.stdout.decode('utf-8')}\n" + f"==stderr==\n" + f"{cp.stderr.decode('utf-8')}\n") + + return cp.stdout.decode("utf-8").rstrip() + + +def get_files(filter=None, paths=None): + filter_arg = (f'--diff-filter={filter}',) if filter else () + paths_arg = ('--', *paths) if paths else () + return git('diff', '--name-only', *filter_arg, COMMIT_RANGE, *paths_arg) + + +def ls_owned_files(codeowners): + """Returns an OrderedDict mapping git patterns from the CODEOWNERS file + to the corresponding list of files found on the filesystem. It + unfortunately does not seem possible to invoke git and re-use + how 'git ignore' and/or 'git attributes' already implement this, + we must re-invent it. + """ + + # TODO: filter out files not in "git ls-files" (e.g., + # twister-out) _if_ the overhead isn't too high for a clean tree. + # + # pathlib.match() doesn't support **, so it looks like we can't + # recursively glob the output of ls-files directly, only real + # files :-( + + pattern2files = collections.OrderedDict() + top_path = Path(GIT_TOP) + + with open(codeowners, "r") as codeo: + for lineno, line in enumerate(codeo, start=1): + + if line.startswith("#") or not line.strip(): + continue + + match = re.match(r"^([^\s,]+)\s+[^\s]+", line) + if not match: + failure(f"Invalid CODEOWNERS line {lineno}\n\t{line}", + file='CODEOWNERS', line=lineno) + continue + + git_patrn = match.group(1) + glob = git_pattern_to_glob(git_patrn) + files = [] + for abs_path in top_path.glob(glob): + # comparing strings is much faster later + files.append(str(abs_path.relative_to(top_path))) + + if not files: + failure(f"Path '{git_patrn}' not found in the tree" + f"but is listed in CODEOWNERS") + + pattern2files[git_patrn] = files + + return pattern2files + + +def git_pattern_to_glob(git_pattern): + """Appends and prepends '**[/*]' when needed. Result has neither a + leading nor a trailing slash. + """ + + if git_pattern.startswith("/"): + ret = git_pattern[1:] + else: + ret = "**/" + git_pattern + + if git_pattern.endswith("/"): + ret = ret + "**/*" + elif os.path.isdir(os.path.join(GIT_TOP, ret)): + failure("Expected '/' after directory '{}' " + "in CODEOWNERS".format(ret)) + + return ret + + +def codeowners(): + codeowners = os.path.join(GIT_TOP, "CODEOWNERS") + if not os.path.exists(codeowners): + err("CODEOWNERS not available in this repo") + + name_changes = get_files(filter="ARCD") + owners_changes = get_files(paths=(codeowners,)) + + if not name_changes and not owners_changes: + # TODO: 1. decouple basic and cheap CODEOWNERS syntax + # validation from the expensive ls_owned_files() scanning of + # the entire tree. 2. run the former always. + return + + logger.info("If this takes too long then cleanup and try again") + patrn2files = ls_owned_files(codeowners) + + # The way git finds Renames and Copies is not "exact science", + # however if one is missed then it will always be reported as an + # Addition instead. + new_files = get_files(filter="ARC").splitlines() + logger.debug(f"New files {new_files}") + + # Convert to pathlib.Path string representation (e.g., + # backslashes 'dir1\dir2\' on Windows) to be consistent + # with ls_owned_files() + new_files = [str(Path(f)) for f in new_files] + + new_not_owned = [] + for newf in new_files: + f_is_owned = False + + for git_pat, owned in patrn2files.items(): + logger.debug(f"Scanning {git_pat} for {newf}") + + if newf in owned: + logger.info(f"{git_pat} matches new file {newf}") + f_is_owned = True + # Unlike github, we don't care about finding any + # more specific owner. + break + + if not f_is_owned: + new_not_owned.append(newf) + + if new_not_owned: + failure("New files added that are not covered in " + "CODEOWNERS:\n\n" + "\n".join(new_not_owned) + + "\n\nPlease add one or more entries in the " + "CODEOWNERS file to cover those files") + + +def init_logs(cli_arg): + # Initializes logging + + global logger + + level = os.environ.get('LOG_LEVEL', "WARN") + + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(levelname)-8s: %(message)s')) + + logger = logging.getLogger('') + logger.addHandler(console) + logger.setLevel(cli_arg or level) + + logger.info("Log init completed, level=%s", + logging.getLevelName(logger.getEffectiveLevel())) + + +def parse_args(): + default_range = 'HEAD~1..HEAD' + parser = argparse.ArgumentParser( + allow_abbrev=False, + description="Check for CODEOWNERS file ownership.") + parser.add_argument('-c', '--commits', default=default_range, + help=f'''Commit range in the form: a..[b], default is + {default_range}''') + parser.add_argument("-v", "--loglevel", choices=['DEBUG', 'INFO', 'WARNING', + 'ERROR', 'CRITICAL'], + help="python logging level") + + return parser.parse_args() + + +def main(): + args = parse_args() + + # The absolute path of the top-level git directory. Initialize it here so + # that issues running Git can be reported to GitHub. + global GIT_TOP + GIT_TOP = git("rev-parse", "--show-toplevel") + + # The commit range passed in --commit, e.g. "HEAD~3" + global COMMIT_RANGE + COMMIT_RANGE = args.commits + + init_logs(args.loglevel) + logger.info(f'Running tests on commit range {COMMIT_RANGE}') + + codeowners() + + sys.exit(failures) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/requirements.txt b/scripts/ci/requirements.txt new file mode 100644 index 000000000..b63cc6ad0 --- /dev/null +++ b/scripts/ci/requirements.txt @@ -0,0 +1,3 @@ +jinja2 +pyyaml +junit-xml diff --git a/scripts/gitlint/ncs.py b/scripts/gitlint/ncs.py new file mode 100644 index 000000000..3ec805550 --- /dev/null +++ b/scripts/gitlint/ncs.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +from gitlint.rules import BoolOption, CommitRule, RuleViolation +import re + +class NCSSauceTags(CommitRule): + """This rule enforces the NCS sauce tag specification. + https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/dm_code_base.html#oss-repositories-downstream-project-history + """ + + # A rule MUST have a human friendly name + name = "ncs-sauce-tags" + + # A rule MUST have a *unique* id, we recommend starting with UC (for User-defined Commit-rule). + id = "UC100" + + options_spec = [BoolOption('enable', False, 'Enable the Sauce Tags Rule')] + + def validate(self, commit): + self.log.debug(f'NCSSauceTags: enable:{self.options["enable"].value} ' \ + f'sha:{commit.sha}') + + if not self.options['enable'].value: + self.log.debug(f'Disabled, skipping') + return + + m = re.match(r'^(Revert\s+\")?\[nrf (mergeup|fromtree|fromlist|noup)\]\s+', + commit.message.title) + if not m: + return [RuleViolation(self.id, 'Title does not contain a sauce tag', + line_nr=1)] + + tag = m.group(2) + + if not tag: + return [RuleViolation(self.id, 'Title does not contain a sauce tag', + line_nr=1)] + self.log.debug(f'Matched sauce tag {tag}') + + if tag == 'mergeup': + if not commit.is_merge_commit: + return [RuleViolation(self.id, + 'mergeup used in a non-merge commit')] + if not re.match(r'^\[nrf mergeup\] Merge upstream up to commit \b([a-f0-9]{40})\b', + commit.message.title): + return [RuleViolation(self.id, 'Invalid mergeup commit title')] + elif tag == 'fromlist': + regex = r'^Upstream PR: https://github\.com/.*/pull/\d+' + matches = re.findall(regex, '\n'.join(commit.message.body), re.MULTILINE) + if len(matches) == 0: + return [RuleViolation(self.id, + 'Missing Upstream PR reference in fromlist commit')] + elif tag == 'fromtree': + regex = r'^\(cherry picked from commit \b([a-f0-9]{40})\b\)' + matches = re.findall(regex, '\n'.join(commit.message.body), re.MULTILINE) + if len(matches) == 0: + return [RuleViolation(self.id, + 'Missing cherry-pick reference in fromtree commit')] diff --git a/scripts/gitlint/zephyr_commit_rules.py b/scripts/gitlint/zephyr_commit_rules.py new file mode 100644 index 000000000..8f9cce21b --- /dev/null +++ b/scripts/gitlint/zephyr_commit_rules.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: Apache-2.0 + +""" +The classes below are examples of user-defined CommitRules. Commit rules are gitlint rules that +act on the entire commit at once. Once the rules are discovered, gitlint will automatically take care of applying them +to the entire commit. This happens exactly once per commit. + +A CommitRule contrasts with a LineRule (see examples/my_line_rules.py) in that a commit rule is only applied once on +an entire commit. This allows commit rules to implement more complex checks that span multiple lines and/or checks +that should only be done once per gitlint run. + +While every LineRule can be implemented as a CommitRule, it's usually easier and more concise to go with a LineRule if +that fits your needs. +""" + +from gitlint.rules import CommitRule, RuleViolation, CommitMessageTitle, LineRule, CommitMessageBody +from gitlint.options import IntOption, StrOption +import re + +class BodyMinLineCount(CommitRule): + # A rule MUST have a human friendly name + name = "body-min-line-count" + + # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule). + id = "UC6" + + # A rule MAY have an option_spec if its behavior should be configurable. + options_spec = [IntOption('min-line-count', 2, "Minimum body line count excluding Signed-off-by")] + + def validate(self, commit): + filtered = [x for x in commit.message.body if not x.lower().startswith("signed-off-by") and x != ''] + line_count = len(filtered) + min_line_count = self.options['min-line-count'].value + if line_count < min_line_count: + message = "Commit message body is empty, should at least have {} line(s).".format(min_line_count) + return [RuleViolation(self.id, message, line_nr=1)] + +class BodyMaxLineCount(CommitRule): + # A rule MUST have a human friendly name + name = "body-max-line-count" + + # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule). + id = "UC1" + + # A rule MAY have an option_spec if its behavior should be configurable. + options_spec = [IntOption('max-line-count', 3, "Maximum body line count")] + + def validate(self, commit): + line_count = len(commit.message.body) + max_line_count = self.options['max-line-count'].value + if line_count > max_line_count: + message = "Commit message body contains too many lines ({0} > {1})".format(line_count, max_line_count) + return [RuleViolation(self.id, message, line_nr=1)] + +class SignedOffBy(CommitRule): + """ This rule will enforce that each commit contains a "Signed-off-by" line. + We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by". + """ + + # A rule MUST have a human friendly name + name = "body-requires-signed-off-by" + + # A rule MUST have an *unique* id, we recommend starting with UC (for User-defined Commit-rule). + id = "UC2" + + def validate(self, commit): + flags = re.UNICODE + flags |= re.IGNORECASE + for line in commit.message.body: + if line.lower().startswith("signed-off-by"): + if not re.search(r"(^)Signed-off-by: ([-'\w.]+) ([-'\w.]+) (.*)", line, flags=flags): + return [RuleViolation(self.id, "Signed-off-by: must have a full name", line_nr=1)] + else: + return + return [RuleViolation(self.id, "Commit message does not contain a 'Signed-off-by:' line", line_nr=1)] + +class TitleMaxLengthRevert(LineRule): + name = "title-max-length-no-revert" + id = "UC5" + target = CommitMessageTitle + options_spec = [IntOption('line-length', 72, "Max line length")] + violation_message = "Commit title exceeds max length ({0}>{1})" + + def validate(self, line, _commit): + max_length = self.options['line-length'].value + if len(line) > max_length and not line.startswith("Revert"): + return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)] + +class TitleStartsWithSubsystem(LineRule): + name = "title-starts-with-subsystem" + id = "UC3" + target = CommitMessageTitle + options_spec = [StrOption('regex', ".*", "Regex the title should match")] + + def validate(self, title, _commit): + regex = self.options['regex'].value + pattern = re.compile(regex, re.UNICODE) + violation_message = "Commit title does not follow [subsystem]: [subject] (and should not start with literal subsys:)" + if not pattern.search(title): + return [RuleViolation(self.id, violation_message, title)] + +class MaxLineLengthExceptions(LineRule): + name = "max-line-length-with-exceptions" + id = "UC4" + target = CommitMessageBody + options_spec = [IntOption('line-length', 80, "Max line length")] + violation_message = "Commit message body line exceeds max length ({0}>{1})" + + def validate(self, line, _commit): + max_length = self.options['line-length'].value + urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', line) + if line.startswith('Signed-off-by'): + return + + if urls: + return + + if len(line) > max_length: + return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)] + +class BodyContainsBlockedTags(LineRule): + name = "body-contains-blocked-tags" + id = "UC7" + target = CommitMessageBody + tags = ["Change-Id"] + + def validate(self, line, _commit): + flags = re.IGNORECASE + for tag in self.tags: + if re.search(rf"^\s*{tag}:", line, flags=flags): + return [RuleViolation(self.id, f"Commit message contains a blocked tag: {tag}")] + return diff --git a/scripts/requirements-fixed.txt b/scripts/requirements-fixed.txt new file mode 100644 index 000000000..345d8ca2f --- /dev/null +++ b/scripts/requirements-fixed.txt @@ -0,0 +1,115 @@ +########################################################### +##### Fixed Python Requirements ##### +########################################################### + +Babel==2.9.1 +Jinja2==2.11.3 +MarkupSafe==1.1.1 +Pillow==9.0.1 +PyYAML==5.4.1 +Pygments==2.7.4 +Sphinx==3.4.3 +aenum==3.0.0 +alabaster==0.7.12 +anytree==2.8.0 +appdirs==1.4.4 +arrow==0.17.0 +astroid==2.15.0 +attrs==20.3.0 +bitarray==2.7.3 +breathe==4.26.1 +canopen==1.2.0 +cbor2==5.4.3 +cbor==1.0.0 +certifi==2020.12.5 +cffi==1.14.4 +chardet==4.0.0 +click==7.1.2 +cmsis-pack-manager==0.2.10 +colorama==0.4.6 +commonmark==0.9.1 +coverage==5.4 +cryptography==3.4.8 +docopt==0.6.2 +docutils==0.16 +ecdsa==0.16.1 +future==0.18.2 +gcovr==4.2 +gitlint==0.15.0 +grpcio-tools==1.51.1 +idna==2.10 +imagesize==1.2.0 +imgtool==1.7.0 +importlib-metadata==3.4.0 +iniconfig==1.1.1 +intelhex==2.3.0 +intervaltree==3.1.0 +isort==5.7.0 +jsonschema==4.4.0 +junit2html==30.0.4 +junitparser==2.8.0 +lazy-object-proxy==1.4.3 +lpc-checksum==2.2.0 +lxml==4.9.1 +mccabe==0.6.1 +milksnake==0.1.5 +mock==4.0.3 +mypy-extensions==0.4.3 +mypy==0.921 +naturalsort==1.5.1 +packaging==20.8 +pluggy==0.13.1 +ply==3.11 +prettytable==2.0.0 +progress==1.5 +psutil==5.8.0 +py==1.10.0 +pycparser==2.20 +pyelftools==0.27 +pygit2==1.10.0 +pykwalify==1.8.0 +pylink-square==0.8.1 +pylint==2.16.4 +pyocd==0.29.0 +pyparsing==2.4.7 +pyserial==3.5 +pytest==6.2.2 +python-can==3.3.4 +python-dateutil==2.8.1 +python-magic==0.4.18 +python-stdnum==1.18 +pytz==2020.5 +pyusb==1.2.0 +qrcode==7.3.1 +recommonmark==0.6.0 +regex==2022.3.15 +requests==2.25.1 +ruamel.yaml.clib==0.2.6 +ruamel.yaml==0.17.21 +sh==1.14.1 +six==1.15.0 +snowballstemmer==2.1.0 +sortedcontainers==2.3.0 +sphinx-rtd-theme==0.5.1 +sphinx-tabs==2.0.0 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-mscgen==0.5 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.4 +sphinxcontrib-svg2pdfconverter==1.1.1 +tabulate==0.8.7 +toml==0.10.2 +tomli==2.0.1 +typed-ast==1.5.4 +typing-extensions==4.4.0 +urllib3==1.26.4 +wcwidth==0.2.5 +west==1.0.0 +wget==3.2 +wrapt==1.12.1 +yamllint==1.30.0 +zcbor==0.5.1 +zipp==3.4.0