diff options
Diffstat (limited to 'utils')
-rwxr-xr-x | utils/abi-compat.sh | 13 | ||||
-rwxr-xr-x | utils/checkstyle.py | 294 | ||||
-rw-r--r-- | utils/codegen/controls.py | 8 | ||||
-rwxr-xr-x | utils/codegen/gen-gst-controls.py | 182 | ||||
-rwxr-xr-x | utils/codegen/gen-tp-header.py | 4 | ||||
-rw-r--r-- | utils/codegen/meson.build | 1 | ||||
-rwxr-xr-x | utils/hooks/pre-push | 11 |
7 files changed, 347 insertions, 166 deletions
diff --git a/utils/abi-compat.sh b/utils/abi-compat.sh index c936ac05..31f61e32 100755 --- a/utils/abi-compat.sh +++ b/utils/abi-compat.sh @@ -156,15 +156,16 @@ create_abi_dump() { # Generate a minimal libcamera build. "lib" and "prefix" are # defined explicitly to avoid system default ambiguities. meson setup "$build" "$worktree" \ - -Dlibdir=lib \ - -Dprefix=/usr/local/ \ - -Ddocumentation=disabled \ -Dcam=disabled \ - -Dqcam=disabled \ + -Ddocumentation=disabled \ -Dgstreamer=disabled \ -Dlc-compliance=disabled \ - -Dtracing=disabled \ - -Dpipelines= + -Dlibdir=lib \ + -Dpipelines= \ + -Dprefix=/usr/local/ \ + -Dpycamera=disabled \ + -Dqcam=disabled \ + -Dtracing=disabled ninja -C "$build" DESTDIR="$install" ninja -C "$build" install diff --git a/utils/checkstyle.py b/utils/checkstyle.py index ab89c0a1..f6229bbd 100755 --- a/utils/checkstyle.py +++ b/utils/checkstyle.py @@ -23,7 +23,6 @@ import subprocess import sys dependencies = { - 'clang-format': True, 'git': True, } @@ -334,43 +333,79 @@ class Amendment(Commit): class ClassRegistry(type): def __new__(cls, clsname, bases, attrs): newclass = super().__new__(cls, clsname, bases, attrs) - if bases: - bases[0].subclasses.append(newclass) - bases[0].subclasses.sort(key=lambda x: getattr(x, 'priority', 0), - reverse=True) + if bases and bases[0] != CheckerBase: + base = bases[0] + + if not hasattr(base, 'subclasses'): + base.subclasses = [] + base.subclasses.append(newclass) + base.subclasses.sort(key=lambda x: getattr(x, 'priority', 0), + reverse=True) return newclass -# ------------------------------------------------------------------------------ -# Commit Checkers -# +class CheckerBase(metaclass=ClassRegistry): + @classmethod + def instances(cls, obj, names): + for instance in cls.subclasses: + if names and instance.__name__ not in names: + continue + if instance.supports(obj): + yield instance -class CommitChecker(metaclass=ClassRegistry): - subclasses = [] + @classmethod + def supports(cls, obj): + if hasattr(cls, 'commit_types'): + return type(obj) in cls.commit_types - def __init__(self): - pass + if hasattr(cls, 'patterns'): + for pattern in cls.patterns: + if fnmatch.fnmatch(os.path.basename(obj), pattern): + return True + + return False - # - # Class methods - # @classmethod - def checkers(cls, commit, names): - for checker in cls.subclasses: - if names and checker.__name__ not in names: - continue - if checker.supports(commit): - yield checker + def all_patterns(cls): + patterns = set() + for instance in cls.subclasses: + if hasattr(instance, 'patterns'): + patterns.update(instance.patterns) + + return patterns @classmethod - def supports(cls, commit): - return type(commit) in cls.commit_types + def check_dependencies(cls): + if not hasattr(cls, 'dependencies'): + return [] + + issues = [] + + for command in cls.dependencies: + if command not in dependencies: + dependencies[command] = shutil.which(command) + + if not dependencies[command]: + issues.append(CommitIssue(f'Missing {command} to run {cls.__name__}')) + + return issues + + +# ------------------------------------------------------------------------------ +# Commit Checkers +# + +class CommitChecker(CheckerBase): + pass class CommitIssue(object): def __init__(self, msg): self.msg = msg + def __str__(self): + return f'{Colours.fg(Colours.Yellow)}{self.msg}{Colours.reset()}' + class HeaderAddChecker(CommitChecker): commit_types = (Commit, StagedChanges, Amendment) @@ -561,37 +596,8 @@ class TrailersChecker(CommitChecker): # Style Checkers # -class StyleChecker(metaclass=ClassRegistry): - subclasses = [] - - def __init__(self): - pass - - # - # Class methods - # - @classmethod - def checkers(cls, filename, names): - for checker in cls.subclasses: - if names and checker.__name__ not in names: - continue - if checker.supports(filename): - yield checker - - @classmethod - def supports(cls, filename): - for pattern in cls.patterns: - if fnmatch.fnmatch(os.path.basename(filename), pattern): - return True - return False - - @classmethod - def all_patterns(cls): - patterns = set() - for checker in cls.subclasses: - patterns.update(checker.patterns) - - return patterns +class StyleChecker(CheckerBase): + pass class StyleIssue(object): @@ -601,21 +607,36 @@ class StyleIssue(object): self.line = line self.msg = msg + def __str__(self): + s = [] + s.append(f'{Colours.fg(Colours.Yellow)}#{self.line_number}: {self.msg}{Colours.reset()}') + if self.line is not None: + s.append(f'{Colours.fg(Colours.Yellow)}+{self.line.rstrip()}{Colours.reset()}') + + if self.position is not None: + # Align the position marker by using the original line with + # all characters except for tabs replaced with spaces. This + # ensures proper alignment regardless of how the code is + # indented. + start = self.position[0] + prefix = ''.join([c if c == '\t' else ' ' for c in self.line[:start]]) + length = self.position[1] - start - 1 + s.append(f' {prefix}^{"~" * length}') + + return '\n'.join(s) + class HexValueChecker(StyleChecker): patterns = ('*.c', '*.cpp', '*.h') regex = re.compile(r'\b0[xX][0-9a-fA-F]+\b') - def __init__(self, content): - super().__init__() - self.__content = content - - def check(self, line_numbers): + @classmethod + def check(cls, content, line_numbers): issues = [] for line_number in line_numbers: - line = self.__content[line_number - 1] + line = content[line_number - 1] match = HexValueChecker.regex.search(line) if not match: continue @@ -639,15 +660,12 @@ class IncludeChecker(StyleChecker): 'cwchar', 'cwctype', 'math.h') include_regex = re.compile(r'^#include <([a-z.]*)>') - def __init__(self, content): - super().__init__() - self.__content = content - - def check(self, line_numbers): + @classmethod + def check(self, content, line_numbers): issues = [] for line_number in line_numbers: - line = self.__content[line_number - 1] + line = content[line_number - 1] match = IncludeChecker.include_regex.match(line) if not match: continue @@ -673,14 +691,11 @@ class LogCategoryChecker(StyleChecker): log_regex = re.compile(r'\bLOG\((Debug|Info|Warning|Error|Fatal)\)') patterns = ('*.cpp',) - def __init__(self, content): - super().__init__() - self.__content = content - - def check(self, line_numbers): + @classmethod + def check(cls, content, line_numbers): issues = [] for line_number in line_numbers: - line = self.__content[line_number-1] + line = content[line_number - 1] match = LogCategoryChecker.log_regex.search(line) if not match: continue @@ -694,14 +709,11 @@ class LogCategoryChecker(StyleChecker): class MesonChecker(StyleChecker): patterns = ('meson.build',) - def __init__(self, content): - super().__init__() - self.__content = content - - def check(self, line_numbers): + @classmethod + def check(cls, content, line_numbers): issues = [] for line_number in line_numbers: - line = self.__content[line_number-1] + line = content[line_number - 1] pos = line.find('\t') if pos != -1: issues.append(StyleIssue(line_number, [pos, pos], line, @@ -710,23 +722,17 @@ class MesonChecker(StyleChecker): class ShellChecker(StyleChecker): + dependencies = ('shellcheck',) patterns = ('*.sh',) results_line_regex = re.compile(r'In - line ([0-9]+):') - def __init__(self, content): - super().__init__() - self.__content = content - - def check(self, line_numbers): + @classmethod + def check(cls, content, line_numbers): issues = [] - data = ''.join(self.__content).encode('utf-8') + data = ''.join(content).encode('utf-8') - try: - ret = subprocess.run(['shellcheck', '-Cnever', '-'], - input=data, stdout=subprocess.PIPE) - except FileNotFoundError: - issues.append(StyleIssue(0, None, None, 'Please install shellcheck to validate shell script additions')) - return issues + ret = subprocess.run(['shellcheck', '-Cnever', '-'], + input=data, stdout=subprocess.PIPE) results = ret.stdout.decode('utf-8').splitlines() for nr, item in enumerate(results): @@ -748,40 +754,12 @@ class ShellChecker(StyleChecker): # Formatters # -class Formatter(metaclass=ClassRegistry): - subclasses = [] - - def __init__(self): - pass - - # - # Class methods - # - @classmethod - def formatters(cls, filename, names): - for formatter in cls.subclasses: - if names and formatter.__name__ not in names: - continue - if formatter.supports(filename): - yield formatter - - @classmethod - def supports(cls, filename): - for pattern in cls.patterns: - if fnmatch.fnmatch(os.path.basename(filename), pattern): - return True - return False - - @classmethod - def all_patterns(cls): - patterns = set() - for formatter in cls.subclasses: - patterns.update(formatter.patterns) - - return patterns +class Formatter(CheckerBase): + pass class CLangFormatter(Formatter): + dependencies = ('clang-format',) patterns = ('*.c', '*.cpp', '*.h') priority = -1 @@ -911,17 +889,13 @@ class IncludeOrderFormatter(Formatter): class Pep8Formatter(Formatter): + dependencies = ('autopep8',) patterns = ('*.py',) @classmethod def format(cls, filename, data): - try: - ret = subprocess.run(['autopep8', '--ignore=E501', '-'], - input=data.encode('utf-8'), stdout=subprocess.PIPE) - except FileNotFoundError: - issues.append(StyleIssue(0, None, None, 'Please install autopep8 to format python additions')) - return issues - + ret = subprocess.run(['autopep8', '--ignore=E501', '-'], + input=data.encode('utf-8'), stdout=subprocess.PIPE) return ret.stdout.decode('utf-8') @@ -940,6 +914,24 @@ class StripTrailingSpaceFormatter(Formatter): # Style checking # +def check_commit(top_level, commit, checkers): + issues = [] + + # Apply the commit checkers first. + for checker in CommitChecker.instances(commit, checkers): + issues_ = checker.check_dependencies() + if issues_: + issues += issues_ + continue + + issues += checker.check(commit, top_level) + + for issue in issues: + print(issue) + + return len(issues) + + def check_file(top_level, commit, filename, checkers): # Extract the line numbers touched by the commit. commit_diff = commit.get_diff(top_level, filename) @@ -955,9 +947,15 @@ def check_file(top_level, commit, filename, checkers): # Format the file after the commit with all formatters and compute the diff # between the unformatted and formatted contents. after = commit.get_file(filename) + issues = [] formatted = after - for formatter in Formatter.formatters(filename, checkers): + for formatter in Formatter.instances(filename, checkers): + issues_ = formatter.check_dependencies() + if issues_: + issues += issues_ + continue + formatted = formatter.format(filename, formatted) after = after.splitlines(True) @@ -970,11 +968,14 @@ def check_file(top_level, commit, filename, checkers): formatted_diff = [hunk for hunk in formatted_diff if hunk.intersects(lines)] # Check for code issues not related to formatting. - issues = [] - for checker in StyleChecker.checkers(filename, checkers): - checker = checker(after) + for checker in StyleChecker.instances(filename, checkers): + issues_ = checker.check_dependencies() + if issues_: + issues += issues_ + continue + for hunk in commit_diff: - issues += checker.check(hunk.side('to').touched) + issues += checker.check(after, hunk.side('to').touched) # Print the detected issues. if len(issues) == 0 and len(formatted_diff) == 0: @@ -988,23 +989,9 @@ def check_file(top_level, commit, filename, checkers): print(hunk) if len(issues): - issues = sorted(issues, key=lambda i: i.line_number) + issues = sorted(issues, key=lambda i: getattr(i, 'line_number', -1)) for issue in issues: - print('%s#%u: %s%s' % (Colours.fg(Colours.Yellow), issue.line_number, - issue.msg, Colours.reset())) - if issue.line is not None: - print('%s+%s%s' % (Colours.fg(Colours.Yellow), issue.line.rstrip(), - Colours.reset())) - - if issue.position is not None: - # Align the position marker by using the original line with - # all characters except for tabs replaced with spaces. This - # ensures proper alignment regardless of how the code is - # indented. - start = issue.position[0] - prefix = ''.join([c if c == '\t' else ' ' for c in issue.line[:start]]) - length = issue.position[1] - start - 1 - print(' ' + prefix + '^' + '~' * length) + print(issue) return len(formatted_diff) + len(issues) @@ -1016,13 +1003,8 @@ def check_style(top_level, commit, checkers): print(title) print(separator) - issues = 0 - # Apply the commit checkers first. - for checker in CommitChecker.checkers(commit, checkers): - for issue in checker.check(commit, top_level): - print('%s%s%s' % (Colours.fg(Colours.Yellow), issue.msg, Colours.reset())) - issues += 1 + issues = check_commit(top_level, commit, checkers) # Filter out files we have no checker for. patterns = set() @@ -1094,7 +1076,7 @@ def main(argv): if args.checkers: args.checkers = args.checkers.split(',') - # Check for required dependencies. + # Check for required common dependencies. for command, mandatory in dependencies.items(): found = shutil.which(command) if mandatory and not found: diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py index 7bafee59..03c77cc6 100644 --- a/utils/codegen/controls.py +++ b/utils/codegen/controls.py @@ -110,3 +110,11 @@ class Control(object): return f"Span<const {typ}, {self.__size}>" else: return f"Span<const {typ}>" + + @property + def element_type(self): + return self.__data.get('type') + + @property + def size(self): + return self.__size diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py new file mode 100755 index 00000000..2601a675 --- /dev/null +++ b/utils/codegen/gen-gst-controls.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2019, Google Inc. +# Copyright (C) 2024, Jaslo Ziska +# +# Authors: +# Laurent Pinchart <laurent.pinchart@ideasonboard.com> +# Jaslo Ziska <jaslo@ziska.de> +# +# Generate gstreamer control properties from YAML + +import argparse +import jinja2 +import re +import sys +import yaml + +from controls import Control + + +exposed_controls = [ + 'AeEnable', 'AeMeteringMode', 'AeConstraintMode', 'AeExposureMode', + 'ExposureValue', 'ExposureTime', 'AnalogueGain', 'AeFlickerPeriod', + 'Brightness', 'Contrast', 'AwbEnable', 'AwbMode', 'ColourGains', + 'Saturation', 'Sharpness', 'ColourCorrectionMatrix', 'ScalerCrop', + 'DigitalGain', 'AfMode', 'AfRange', 'AfSpeed', 'AfMetering', 'AfWindows', + 'LensPosition', 'Gamma', +] + + +def find_common_prefix(strings): + prefix = strings[0] + + for string in strings[1:]: + while string[:len(prefix)] != prefix and prefix: + prefix = prefix[:len(prefix) - 1] + if not prefix: + break + + return prefix + + +def format_description(description): + # Substitute doxygen keywords \sa (see also) and \todo + description = re.sub(r'\\sa((?: \w+)+)', + lambda match: 'See also: ' + ', '.join( + map(kebab_case, match.group(1).strip().split(' ')) + ) + '.', description) + description = re.sub(r'\\todo', 'Todo:', description) + + description = description.strip().split('\n') + return '\n'.join([ + '"' + line.replace('\\', r'\\').replace('"', r'\"') + ' "' for line in description if line + ]).rstrip() + + +# Custom filter to allow indenting by a string prior to Jinja version 3.0 +# +# This function can be removed and the calls to indent_str() replaced by the +# built-in indent() filter when dropping Jinja versions older than 3.0 +def indent_str(s, indention): + s += '\n' + + lines = s.splitlines() + rv = lines.pop(0) + + if lines: + rv += '\n' + '\n'.join( + indention + line if line else line for line in lines + ) + + return rv + + +def snake_case(s): + return ''.join([ + c.isupper() and ('_' + c.lower()) or c for c in s + ]).strip('_') + + +def kebab_case(s): + return snake_case(s).replace('_', '-') + + +def extend_control(ctrl): + if ctrl.vendor != 'libcamera': + ctrl.namespace = f'{ctrl.vendor}::' + ctrl.vendor_prefix = f'{ctrl.vendor}-' + else: + ctrl.namespace = '' + ctrl.vendor_prefix = '' + + ctrl.is_array = ctrl.size is not None + + if ctrl.is_enum: + # Remove common prefix from enum variant names + prefix = find_common_prefix([enum.name for enum in ctrl.enum_values]) + for enum in ctrl.enum_values: + enum.gst_name = kebab_case(enum.name.removeprefix(prefix)) + + ctrl.gtype = 'enum' + ctrl.default = '0' + elif ctrl.element_type == 'bool': + ctrl.gtype = 'boolean' + ctrl.default = 'false' + elif ctrl.element_type == 'float': + ctrl.gtype = 'float' + ctrl.default = '0' + ctrl.min = '-G_MAXFLOAT' + ctrl.max = 'G_MAXFLOAT' + elif ctrl.element_type == 'int32_t': + ctrl.gtype = 'int' + ctrl.default = '0' + ctrl.min = 'G_MININT' + ctrl.max = 'G_MAXINT' + elif ctrl.element_type == 'int64_t': + ctrl.gtype = 'int64' + ctrl.default = '0' + ctrl.min = 'G_MININT64' + ctrl.max = 'G_MAXINT64' + elif ctrl.element_type == 'uint8_t': + ctrl.gtype = 'uchar' + ctrl.default = '0' + ctrl.min = '0' + ctrl.max = 'G_MAXUINT8' + elif ctrl.element_type == 'Rectangle': + ctrl.is_rectangle = True + ctrl.default = '0' + ctrl.min = '0' + ctrl.max = 'G_MAXINT' + else: + raise RuntimeError(f'The type `{ctrl.element_type}` is unknown') + + return ctrl + + +def main(argv): + # Parse command line arguments + parser = argparse.ArgumentParser() + parser.add_argument('--output', '-o', metavar='file', type=str, + help='Output file name. Defaults to standard output if not specified.') + parser.add_argument('--template', '-t', dest='template', type=str, required=True, + help='Template file name.') + parser.add_argument('input', type=str, nargs='+', + help='Input file name.') + + args = parser.parse_args(argv[1:]) + + controls = {} + for input in args.input: + data = yaml.safe_load(open(input, 'rb').read()) + + vendor = data['vendor'] + ctrls = controls.setdefault(vendor, []) + + for ctrl in data['controls']: + ctrl = Control(*ctrl.popitem(), vendor) + + if ctrl.name in exposed_controls: + ctrls.append(extend_control(ctrl)) + + data = {'controls': list(controls.items())} + + env = jinja2.Environment() + env.filters['format_description'] = format_description + env.filters['indent_str'] = indent_str + env.filters['snake_case'] = snake_case + env.filters['kebab_case'] = kebab_case + template = env.from_string(open(args.template, 'r', encoding='utf-8').read()) + string = template.render(data) + + if args.output: + with open(args.output, 'w', encoding='utf-8') as output: + output.write(string) + else: + sys.stdout.write(string) + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/utils/codegen/gen-tp-header.py b/utils/codegen/gen-tp-header.py index 83606c32..6769c7ce 100755 --- a/utils/codegen/gen-tp-header.py +++ b/utils/codegen/gen-tp-header.py @@ -6,7 +6,6 @@ # # Generate header file to contain lttng tracepoints -import datetime import jinja2 import pathlib import os @@ -20,7 +19,6 @@ def main(argv): output = argv[2] template = argv[3] - year = datetime.datetime.now().year path = pathlib.Path(output).absolute().relative_to(argv[1]) source = '' @@ -28,7 +26,7 @@ def main(argv): source += open(fname, 'r', encoding='utf-8').read() + '\n\n' template = jinja2.Template(open(template, 'r', encoding='utf-8').read()) - string = template.render(year=year, path=path, source=source) + string = template.render(path=path, source=source) f = open(output, 'w', encoding='utf-8').write(string) diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build index adf33bba..904dd66d 100644 --- a/utils/codegen/meson.build +++ b/utils/codegen/meson.build @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml'] gen_controls = files('gen-controls.py') gen_formats = files('gen-formats.py') +gen_gst_controls = files('gen-gst-controls.py') gen_header = files('gen-header.sh') gen_ipa_pub_key = files('gen-ipa-pub-key.py') gen_tracepoints = files('gen-tp-header.py') diff --git a/utils/hooks/pre-push b/utils/hooks/pre-push index 9918b286..68dcbd0c 100755 --- a/utils/hooks/pre-push +++ b/utils/hooks/pre-push @@ -68,7 +68,7 @@ do fi # 2. The commit message shall have Signed-off-by lines - # corresponding the committer and the author. + # corresponding the committer, author, and all co-developers. committer=$(echo "$msg" | grep '^committer ' | head -1 | \ cut -d ' ' -f 2- | rev | cut -d ' ' -f 3- | rev) if ! echo -E "$msg" | grep -F -q "Signed-off-by: ${committer}" @@ -85,6 +85,15 @@ do errors=$((errors+1)) fi + while read -r codev + do + if ! echo -E "$msg" | grep -F -q "Signed-off-by: ${codev}" + then + echo >&2 "Missing co-developer '${codev}' Signed-off-by in commit $commit" + errors=$((errors+1)) + fi + done < <(echo "$msg" | grep '^Co-developed-by: ' | cut -d ' ' -f 2-) + # 3. A Reviewed-by or Acked-by is required. if ! echo -E "$msg" | grep -q '^\(Reviewed\|Acked\)-by: ' then |