From 2bc688001cfbbe4a6b58d3fa4a08e7c3da46df54 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 10 Jun 2026 18:28:58 -0600 Subject: [PATCH 01/24] Add shared C++ tooling scripts Centralize LiveKit clang-format and clang-tidy configuration so C++ repos can consume the same checks through a submodule. Co-authored-by: Cursor --- README.md | 17 +- configs/.clang-format | 10 + configs/.clang-tidy | 42 ++++ docs/tools.md | 58 +++++ scripts/clang-format.sh | 362 +++++++++++++++++++++++++++++ scripts/clang-tidy.sh | 425 ++++++++++++++++++++++++++++++++++ scripts/install-pre-commit.sh | 79 +++++++ 7 files changed, 992 insertions(+), 1 deletion(-) create mode 100644 configs/.clang-format create mode 100644 configs/.clang-tidy create mode 100644 docs/tools.md create mode 100755 scripts/clang-format.sh create mode 100755 scripts/clang-tidy.sh create mode 100755 scripts/install-pre-commit.sh diff --git a/README.md b/README.md index b1170fd..b9a2c70 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # cpp-tools -Standardized set of tools for use in LiveKit C++ projects. + +Standardized tools for LiveKit C++ projects. + +This repository is intended to be consumed as a git submodule by LiveKit C++ +repositories. It provides the shared source of truth for: + +- `clang-format` style (`configs/.clang-format`) +- `clang-tidy` checks (`configs/.clang-tidy`) +- local and CI wrapper scripts under `scripts/` +- documentation for the shared tooling under `docs/` + +Consumer repositories should expose root-level symlinks for `.clang-format` and +`.clang-tidy` so editor integrations can find them, and should pass +project-specific paths/build filters to the shared scripts. + +See [docs/tools.md](docs/tools.md) for usage. diff --git a/configs/.clang-format b/configs/.clang-format new file mode 100644 index 0000000..7707fc5 --- /dev/null +++ b/configs/.clang-format @@ -0,0 +1,10 @@ +--- +BasedOnStyle: Google +Language: Cpp +Standard: c++17 + +# LiveKit modifications to Google style below + +ColumnLimit: 120 # more width on modern screens +SpacesBeforeTrailingComments: 1 # one space for trailing namespace comments, e.g. `} // namespace foo` (Google uses 2) +AccessModifierOffset: -2 # left-align public/protected/private with the class keyword (Google indents by 1) diff --git a/configs/.clang-tidy b/configs/.clang-tidy new file mode 100644 index 0000000..d332d0e --- /dev/null +++ b/configs/.clang-tidy @@ -0,0 +1,42 @@ +Checks: > + -*, + bugprone-*, + misc-const-correctness, + performance-*, + modernize-*, + readability-misleading-indentation, + readability-redundant-smartptr-get, + readability-identifier-naming, + -bugprone-easily-swappable-parameters, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + -modernize-type-traits, + -modernize-use-auto, + -modernize-use-nodiscard, + -modernize-return-braced-init-list, + -performance-enum-size, + -readability-braces-around-statements, + +# These warnings have determined to be critical and are as such treated as errors +WarningsAsErrors: > + clang-analyzer-*, + bugprone-use-after-move, + bugprone-dangling-handle, + bugprone-infinite-loop, + bugprone-narrowing-conversions, + bugprone-undefined-memory-manipulation, + bugprone-move-forwarding-reference, + bugprone-incorrect-roundings, + bugprone-sizeof-expression, + bugprone-string-literal-with-embedded-nul, + bugprone-suspicious-memset-usage, + +FormatStyle: file + +CheckOptions: + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.MethodCase + value: camelBack diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 0000000..f670391 --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,58 @@ +# LiveKit C++ Tools + +This repository owns shared C++ formatting and static-analysis policy for +LiveKit C++ projects. + +## Configs + +Consumer repositories should expose root-level symlinks so editor extensions +and local command-line tools can discover the standard config files: + +```bash +ln -s cpp-tools/configs/.clang-format .clang-format +ln -s cpp-tools/configs/.clang-tidy .clang-tidy +``` + +## clang-format + +Run from a consuming repository root: + +```bash +./cpp-tools/scripts/clang-format.sh +./cpp-tools/scripts/clang-format.sh --fix +``` + +By default the script scans existing `src/`, `include/`, and `benchmarks/` +trees. Repos with different layouts can pass `--path` repeatedly or set +`CLANG_FORMAT_PATHS` to a colon-separated list. + +## clang-tidy + +Run after the consuming repository has generated `compile_commands.json`: + +```bash +./cpp-tools/scripts/clang-tidy.sh +./cpp-tools/scripts/clang-tidy.sh --fail-on-warning +``` + +Project-specific behavior is configured with command-line flags or environment +variables: + +```bash +./cpp-tools/scripts/clang-tidy.sh \ + --build-dir build-release \ + --file-regex '.*src/.*\.(c|cpp|cc|cxx)$' \ + --header-filter '.*/(include|src)/.*\.(h|hpp)$' \ + --exclude-header-filter '(.*/tests/.*)|(.*/build-[^/]*/.*)' +``` + +## Pre-commit Hook + +Install the shared auto-format hook from a consuming repository: + +```bash +./cpp-tools/scripts/install-pre-commit.sh +``` + +The hook formats staged C/C++ files and re-stages any files rewritten by +`clang-format`. diff --git a/scripts/clang-format.sh b/scripts/clang-format.sh new file mode 100755 index 0000000..e0d3cc9 --- /dev/null +++ b/scripts/clang-format.sh @@ -0,0 +1,362 @@ +#!/usr/bin/env bash +# +# Copyright 2026 LiveKit +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: clang-format.sh [OPTIONS] [FILE...] + +Run clang-format against a consuming repository. Defaults to check-only +(--dry-run --Werror); pass --fix to rewrite files in place. + +Options: + -h, --help, -? + Show this help and exit. + --repo-root PATH + Repository root to operate on. Defaults to REPO_ROOT, then + `git rev-parse --show-toplevel` from the current directory. + --path PATH + Tracked path or glob to scan when FILE... is omitted. May be repeated. + Defaults to existing src/, include/, and benchmarks/ trees. + --fix, -i + Apply formatting in place. + --github-actions, --gh + Force GitHub Actions annotation + step-summary mode. + +Environment: + REPO_ROOT + Same as --repo-root. + CLANG_FORMAT_PATHS + Colon-separated list used when --path is not supplied. +EOF +} + +repo_root="${REPO_ROOT:-}" +paths=() +explicit_files=() +fix_mode=0 +ci_mode=0 +if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + ci_mode=1 +fi + +while (($#)); do + case "$1" in + -h|--help|-\?) + usage + exit 0 + ;; + --repo-root) + if (($# < 2)); then + echo "ERROR: --repo-root requires a path." >&2 + exit 2 + fi + repo_root="$2" + shift 2 + ;; + --path) + if (($# < 2)); then + echo "ERROR: --path requires a path." >&2 + exit 2 + fi + paths+=("$2") + shift 2 + ;; + --fix|-i) + fix_mode=1 + shift + ;; + --github-actions|--gh) + ci_mode=1 + shift + ;; + --) + shift + explicit_files+=("$@") + break + ;; + --*|-*) + echo "ERROR: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + *) + explicit_files+=("$1") + shift + ;; + esac +done + +if [[ -z "${repo_root}" ]]; then + repo_root="$(git rev-parse --show-toplevel)" +fi +repo_root="$(cd "${repo_root}" && pwd -P)" +cd "${repo_root}" + +if ! command -v clang-format >/dev/null 2>&1; then + echo "ERROR: clang-format not found in PATH." >&2 + echo "Install: brew install clang-format (macOS)" >&2 + echo " apt install clang-format (Linux)" >&2 + exit 1 +fi + +clang-format --version + +if (( ${#paths[@]} == 0 )) && [[ -n "${CLANG_FORMAT_PATHS:-}" ]]; then + IFS=':' read -r -a paths <<< "${CLANG_FORMAT_PATHS}" +fi +if (( ${#paths[@]} == 0 )); then + for candidate in src include benchmarks; do + if [[ -e "${candidate}" ]]; then + paths+=("${candidate}") + fi + done +fi + +files=() +if (( ${#explicit_files[@]} > 0 )); then + files=("${explicit_files[@]}") +else + if (( ${#paths[@]} == 0 )); then + echo "clang-format: no paths to scan." + exit 0 + fi + while IFS= read -r -d '' path; do + files+=("${path}") + done < <(git ls-files -z -- \ + "${paths[@]}" \ + ':!*.md' \ + ':!*.txt' \ + ':!*.cmake' \ + ':!CMakeLists.txt' \ + | while IFS= read -r -d '' path; do + case "${path}" in + *.c|*.cc|*.cpp|*.cxx|*.h|*.hpp|*.hxx) printf '%s\0' "${path}" ;; + esac + done) +fi + +file_count=${#files[@]} +if (( file_count == 0 )); then + echo "clang-format: no files to process." + exit 0 +fi + +log="clang-format.log" +: > "${log}" + +emit_annotations() { + local log_file="$1" + local workspace="${GITHUB_WORKSPACE:-${PWD}}" + local line path lineno col message rel_path + + while IFS= read -r line; do + [[ "${line}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(error|warning):[[:space:]]+(.+)[[:space:]]\[-Wclang-format-violations\][[:space:]]*$ ]] || continue + path="${BASH_REMATCH[1]}" + lineno="${BASH_REMATCH[2]}" + col="${BASH_REMATCH[3]}" + message="${BASH_REMATCH[5]}" + rel_path="${path#${workspace}/}" + message="${message//$'%'/%25}" + message="${message//$'\r'/%0D}" + message="${message//$'\n'/%0A}" + printf '::error file=%s,line=%s,col=%s,title=clang-format::%s\n' \ + "${rel_path}" "${lineno}" "${col}" "${message}" + done < "${log_file}" +} + +write_step_summary() { + local log_file="$1" + local summary_file="${GITHUB_STEP_SUMMARY:-}" + [[ -n "${summary_file}" ]] || return 0 + + local workspace="${GITHUB_WORKSPACE:-${PWD}}" + local files_tsv + files_tsv="$(mktemp -t fmt-files.XXXXXX)" + + local sline spath slineno + declare -A seen=() + while IFS= read -r sline; do + [[ "${sline}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(error|warning):[[:space:]]+(.+)[[:space:]]\[-Wclang-format-violations\][[:space:]]*$ ]] || continue + spath="${BASH_REMATCH[1]#${workspace}/}" + slineno="${BASH_REMATCH[2]}" + if [[ -z "${seen[${spath}]:-}" ]]; then + seen[${spath}]=1 + printf '%s\t%s\n' "${spath}" "${slineno}" >> "${files_tsv}" + fi + done < "${log_file}" + + local violation_files + violation_files=$(wc -l < "${files_tsv}" | tr -d ' ') + local repo_url="" + if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_REPOSITORY:-}" ]]; then + local blob_sha="${FORMAT_BLOB_SHA:-${GITHUB_SHA:-}}" + if [[ -n "${blob_sha}" ]]; then + repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${blob_sha}" + fi + fi + + { + echo "## clang-format results" + echo + if (( violation_files == 0 )); then + echo ":white_check_mark: All files are properly formatted." + else + echo ":x: ${violation_files} file(s) need formatting." + echo + echo "
Files needing formatting" + echo + echo '| File |' + echo '|------|' + while IFS=$'\t' read -r path lineno; do + local file_cell + if [[ -n "${repo_url}" && "${path}" != /* ]]; then + file_cell="[\`${path}\`](${repo_url}/${path}#L${lineno})" + else + file_cell="\`${path}\`" + fi + printf '| %s |\n' "${file_cell}" + done < "${files_tsv}" + echo + echo "
" + echo + echo "Run \`./cpp-tools/scripts/clang-format.sh --fix\` locally to apply formatting." + fi + } >> "${summary_file}" + + rm -f "${files_tsv}" +} + +print_stdout_summary() { + local log_file="$1" + local total="$2" + local files_tsv + files_tsv="$(mktemp -t fmt-stdout.XXXXXX)" + + local line spath + declare -A seen=() + while IFS= read -r line; do + [[ "${line}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(error|warning):[[:space:]]+(.+)[[:space:]]\[-Wclang-format-violations\][[:space:]]*$ ]] || continue + spath="${BASH_REMATCH[1]}" + if [[ -z "${seen[${spath}]:-}" ]]; then + seen[${spath}]=1 + echo "${spath}" >> "${files_tsv}" + fi + done < "${log_file}" + + local violation_files + violation_files=$(wc -l < "${files_tsv}" | tr -d ' ') + + echo "------------------------------------------------------------" + if (( violation_files == 0 )); then + printf 'clang-format summary: clean (%d file(s) checked)\n' "${total}" + else + printf 'clang-format summary: %d of %d file(s) need formatting\n' \ + "${violation_files}" "${total}" + echo " files:" + while IFS= read -r f; do + printf ' %s\n' "${f}" + done < "${files_tsv}" + echo + echo " Run './cpp-tools/scripts/clang-format.sh --fix' to apply formatting." + fi + echo "------------------------------------------------------------" + + rm -f "${files_tsv}" + __FMT_VIOLATION_FILES="${violation_files}" +} + +__hash_files() { + if (( ${#files[@]} == 0 )); then + return + fi + printf '%s\n' "${files[@]}" | git hash-object --stdin-paths +} + +pre_hashes=() +if (( fix_mode == 1 )); then + while IFS= read -r __h; do + pre_hashes+=("${__h}") + done < <(__hash_files) +fi + +set +e +if (( fix_mode == 1 )); then + printf '%s\0' "${files[@]}" | xargs -0 clang-format -i >"${log}" 2>&1 + rc=$? +else + printf '%s\0' "${files[@]}" | xargs -0 clang-format --dry-run --Werror >"${log}" 2>&1 + rc=$? +fi +set -e + +changed_files=() +if (( fix_mode == 1 )); then + post_hashes=() + while IFS= read -r __h; do + post_hashes+=("${__h}") + done < <(__hash_files) + for i in "${!files[@]}"; do + if [[ "${pre_hashes[$i]:-}" != "${post_hashes[$i]:-}" ]]; then + changed_files+=("${files[$i]}") + fi + done +fi + +if (( fix_mode == 0 )); then + cat "${log}" +fi + +if [[ "${ci_mode}" == "1" ]] && (( fix_mode == 0 )); then + emit_annotations "${log}" + write_step_summary "${log}" +fi + +__FMT_VIOLATION_FILES=0 +if (( fix_mode == 1 )); then + echo "------------------------------------------------------------" + if (( ${#changed_files[@]} == 0 )); then + printf 'clang-format summary: clean (0 of %d file(s) needed formatting)\n' "${file_count}" + else + printf 'clang-format summary: formatted %d of %d file(s)\n' "${#changed_files[@]}" "${file_count}" + echo " files:" + for __cf in "${changed_files[@]}"; do + printf ' %s\n' "${__cf}" + done + fi + echo "------------------------------------------------------------" +else + print_stdout_summary "${log}" "${file_count}" +fi + +if [[ -s "${log}" ]]; then + echo "Results written to: $(pwd)/${log}" +else + rm -f "${log}" +fi + +if (( fix_mode == 1 )); then + exit "${rc}" +fi + +if (( rc == 0 )); then + exit 0 +elif (( rc == 123 )) || (( __FMT_VIOLATION_FILES > 0 )); then + exit 1 +else + exit "${rc}" +fi diff --git a/scripts/clang-tidy.sh b/scripts/clang-tidy.sh new file mode 100755 index 0000000..55cb8f5 --- /dev/null +++ b/scripts/clang-tidy.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# +# Copyright 2026 LiveKit +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: clang-tidy.sh [OPTIONS] [FILE...] [run-clang-tidy args...] + +Run clang-tidy against a consuming repository. The repository must have a +compile_commands.json under the configured build directory. + +Options: + -h, --help, -? + Show this help and exit. + --repo-root PATH + Repository root to operate on. Defaults to REPO_ROOT, then + `git rev-parse --show-toplevel` from the current directory. + --build-dir PATH + Build directory containing compile_commands.json. Default: + CLANG_TIDY_BUILD_DIR or build-release. + --file-regex REGEX + run-clang-tidy target regex used when FILE... is omitted. Default: + CLANG_TIDY_FILE_REGEX or all non-test src/*.c/cpp/cc/cxx files. + --header-filter REGEX + Forwarded to run-clang-tidy as -header-filter. + --exclude-header-filter REGEX + Forwarded to run-clang-tidy as -exclude-header-filter. + --require-generated-protobuf PATH + Require PATH to contain *.pb.h before running. May be relative to + the repo root. Useful for protobuf-backed SDKs. + --fix + Apply fixes in place (forwarded to run-clang-tidy as -fix). + --github-actions, --gh + Force GitHub Actions annotation + step-summary mode. + --fail-on-warning, --strict + Exit non-zero when any warning is emitted. + +Environment: + REPO_ROOT, CLANG_TIDY_BUILD_DIR, CLANG_TIDY_FILE_REGEX, + CLANG_TIDY_HEADER_FILTER, CLANG_TIDY_EXCLUDE_HEADER_FILTER, + CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF + Environment equivalents for the matching options above. +EOF +} + +repo_root="${REPO_ROOT:-}" +build_dir="${CLANG_TIDY_BUILD_DIR:-build-release}" +default_file_regex='^(?!.*/(_deps|build-[^/]*|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' +file_regex="${CLANG_TIDY_FILE_REGEX:-${default_file_regex}}" +header_filter="${CLANG_TIDY_HEADER_FILTER:-}" +exclude_header_filter="${CLANG_TIDY_EXCLUDE_HEADER_FILTER:-}" +required_proto_dir="${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF:-}" +ci_mode=0 +fail_on_warning=0 +forward_args=() +explicit_files=() + +if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + ci_mode=1 +fi + +while (($#)); do + case "$1" in + -h|--help|-\?) + usage + exit 0 + ;; + --repo-root) + if (($# < 2)); then + echo "ERROR: --repo-root requires a path." >&2 + exit 2 + fi + repo_root="$2" + shift 2 + ;; + --build-dir) + if (($# < 2)); then + echo "ERROR: --build-dir requires a path." >&2 + exit 2 + fi + build_dir="$2" + shift 2 + ;; + --file-regex) + if (($# < 2)); then + echo "ERROR: --file-regex requires a regex." >&2 + exit 2 + fi + file_regex="$2" + shift 2 + ;; + --header-filter) + if (($# < 2)); then + echo "ERROR: --header-filter requires a regex." >&2 + exit 2 + fi + header_filter="$2" + shift 2 + ;; + --exclude-header-filter) + if (($# < 2)); then + echo "ERROR: --exclude-header-filter requires a regex." >&2 + exit 2 + fi + exclude_header_filter="$2" + shift 2 + ;; + --require-generated-protobuf) + if (($# < 2)); then + echo "ERROR: --require-generated-protobuf requires a path." >&2 + exit 2 + fi + required_proto_dir="$2" + shift 2 + ;; + --fix) + forward_args+=("-fix") + shift + ;; + --github-actions|--gh) + ci_mode=1 + shift + ;; + --fail-on-warning|--strict) + fail_on_warning=1 + shift + ;; + --) + shift + forward_args+=("$@") + break + ;; + --*) + echo "ERROR: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + -*) + forward_args+=("$1") + shift + ;; + *) + if [[ -f "$1" ]]; then + explicit_files+=("$1") + else + forward_args+=("$1") + fi + shift + ;; + esac +done + +if [[ -z "${repo_root}" ]]; then + repo_root="$(git rev-parse --show-toplevel)" +fi +repo_root="$(cd "${repo_root}" && pwd -P)" +cd "${repo_root}" + +if [[ ! -f "${build_dir}/compile_commands.json" ]]; then + echo "ERROR: ${build_dir}/compile_commands.json not found." >&2 + echo "Run the consuming repository's release/configure build first." >&2 + exit 1 +fi + +if [[ -n "${required_proto_dir}" ]]; then + if [[ ! -d "${required_proto_dir}" ]] || ! compgen -G "${required_proto_dir}/*.pb.h" >/dev/null; then + echo "ERROR: no generated protobuf headers found in ${required_proto_dir}/." >&2 + echo "Run the consuming repository's protobuf/header generation step first." >&2 + exit 1 + fi +fi + +if ! command -v run-clang-tidy >/dev/null 2>&1; then + echo "ERROR: run-clang-tidy not found in PATH." >&2 + echo "Install LLVM: brew install llvm (macOS)" >&2 + echo " apt install clang-tools-NN (Linux)" >&2 + exit 1 +fi + +extra_args=(-extra-arg=-Wno-gnu-zero-variadic-macro-arguments) +if [[ "$(uname)" == "Darwin" ]]; then + sdk_path="$(xcrun --show-sdk-path 2>/dev/null || true)" + if [[ -n "${sdk_path}" ]]; then + extra_args+=(-extra-arg="-isysroot${sdk_path}") + fi +fi + +filter_args=() +if [[ -n "${header_filter}" ]]; then + filter_args+=("-header-filter=${header_filter}") +fi +if [[ -n "${exclude_header_filter}" ]]; then + filter_args+=("-exclude-header-filter=${exclude_header_filter}") +fi + +if command -v nproc >/dev/null 2>&1; then + jobs=$(nproc) +else + jobs=$(sysctl -n hw.ncpu 2>/dev/null || echo 4) +fi + +emit_annotations() { + local log="$1" + local workspace="${GITHUB_WORKSPACE:-${PWD}}" + local line path lineno col severity message check rel_path + + while IFS= read -r line; do + [[ "${line}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(warning|error):[[:space:]]+(.+)[[:space:]]\[([^]]+)\][[:space:]]*$ ]] || continue + path="${BASH_REMATCH[1]}" + lineno="${BASH_REMATCH[2]}" + col="${BASH_REMATCH[3]}" + severity="${BASH_REMATCH[4]}" + message="${BASH_REMATCH[5]}" + check="${BASH_REMATCH[6]}" + check="${check%,-warnings-as-errors}" + rel_path="${path#${workspace}/}" + message="${message//$'%'/%25}" + message="${message//$'\r'/%0D}" + message="${message//$'\n'/%0A}" + printf '::%s file=%s,line=%s,col=%s,title=clang-tidy (%s)::%s\n' \ + "${severity}" "${rel_path}" "${lineno}" "${col}" "${check}" "${message}" + done < "${log}" +} + +check_link() { + local name="$1" + local module="${name%%-*}" + local rest="${name#*-}" + case "${name}" in + clang-diagnostic-*) + printf '`%s`' "${name}" + ;; + clang-analyzer-*) + printf '[`%s`](https://clang.llvm.org/docs/analyzer/checkers.html)' "${name}" + ;; + *) + printf '[`%s`](https://clang.llvm.org/extra/clang-tidy/checks/%s/%s.html)' \ + "${name}" "${module}" "${rest}" + ;; + esac +} + +write_step_summary() { + local log="$1" + local summary_file="${GITHUB_STEP_SUMMARY:-}" + [[ -n "${summary_file}" ]] || return 0 + + local workspace="${GITHUB_WORKSPACE:-${PWD}}" + local findings_tsv + findings_tsv="$(mktemp -t tidy-findings.XXXXXX)" + + local sline spath slineno scol sseverity smessage scheck + while IFS= read -r sline; do + [[ "${sline}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(warning|error):[[:space:]]+(.+)[[:space:]]\[([^]]+)\][[:space:]]*$ ]] || continue + spath="${BASH_REMATCH[1]#${workspace}/}" + slineno="${BASH_REMATCH[2]}" + scol="${BASH_REMATCH[3]}" + sseverity="${BASH_REMATCH[4]}" + smessage="${BASH_REMATCH[5]}" + scheck="${BASH_REMATCH[6]}" + scheck="${scheck%,-warnings-as-errors}" + printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${sseverity}" "${spath}" "${slineno}" "${scol}" "${scheck}" "${smessage}" >> "${findings_tsv}" + done < "${log}" + + local warnings errors total + warnings=$(awk -F'\t' '$1=="warning"{c++} END{print c+0}' "${findings_tsv}") + errors=$(awk -F'\t' '$1=="error"{c++} END{print c+0}' "${findings_tsv}") + total=$((warnings + errors)) + + local repo_url="" + if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_REPOSITORY:-}" ]]; then + local blob_sha="${TIDY_BLOB_SHA:-${GITHUB_SHA:-}}" + if [[ -n "${blob_sha}" ]]; then + repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${blob_sha}" + fi + fi + + { + echo "## clang-tidy results" + echo + if (( total == 0 )); then + echo ":white_check_mark: All files passed the configured checks." + else + echo "| Severity | Count |" + echo "|----------|-------|" + echo "| :x: Error | ${errors} |" + echo "| :warning: Warning | ${warnings} |" + echo + echo "### Top checks" + echo + echo '| Check | Count |' + echo '|-------|-------|' + awk -F'\t' '{print $5}' "${findings_tsv}" \ + | sort | uniq -c | sort -rn | head -5 \ + | while read -r count name; do + printf '| %s | %d |\n' "$(check_link "${name}")" "${count}" + done + echo + echo "
All ${total} findings" + echo + echo '| Severity | File | Check | Message |' + echo '|----------|------|-------|---------|' + { + awk -F'\t' '$1=="error"' "${findings_tsv}" + awk -F'\t' '$1=="warning"' "${findings_tsv}" + } | while IFS=$'\t' read -r sev path lineno col check msg; do + msg="${msg//|/\\|}" + local icon label file_cell + if [[ "${sev}" == "error" ]]; then + icon=':x:' + label='Error' + else + icon=':warning:' + label='Warning' + fi + if [[ -n "${repo_url}" && "${path}" != /* ]]; then + file_cell="[\`${path}:${lineno}\`](${repo_url}/${path}#L${lineno})" + else + file_cell="\`${path}:${lineno}\`" + fi + printf '| %s %s | %s | %s | %s |\n' \ + "${icon}" "${label}" "${file_cell}" "$(check_link "${check}")" "${msg}" + done + echo + echo "
" + echo + fi + } >> "${summary_file}" + + rm -f "${findings_tsv}" +} + +print_stdout_summary() { + local log="$1" + local checks_tsv + checks_tsv="$(mktemp -t tidy-stdout.XXXXXX)" + + local line severity check + while IFS= read -r line; do + [[ "${line}" =~ ^(.+):([0-9]+):([0-9]+):[[:space:]]+(warning|error):[[:space:]]+(.+)[[:space:]]\[([^]]+)\][[:space:]]*$ ]] || continue + severity="${BASH_REMATCH[4]}" + check="${BASH_REMATCH[6]}" + check="${check%,-warnings-as-errors}" + printf '%s\t%s\n' "${severity}" "${check}" >> "${checks_tsv}" + done < "${log}" + + local warnings errors + warnings=$(awk -F'\t' '$1=="warning"{c++} END{print c+0}' "${checks_tsv}") + errors=$(awk -F'\t' '$1=="error"{c++} END{print c+0}' "${checks_tsv}") + + echo "------------------------------------------------------------" + if (( warnings == 0 && errors == 0 )); then + echo "clang-tidy summary: clean (0 warnings, 0 errors)" + else + printf 'clang-tidy summary: %d warning(s), %d error(s)\n' "${warnings}" "${errors}" + echo " by check:" + awk -F'\t' '{print $2}' "${checks_tsv}" \ + | sort | uniq -c | sort -rn \ + | while read -r count name; do + printf ' %-50s %d\n' "${name}" "${count}" + done + fi + echo "------------------------------------------------------------" + + rm -f "${checks_tsv}" + __TIDY_WARNINGS="${warnings}" + __TIDY_ERRORS="${errors}" +} + +log="clang-tidy.log" +if (( ${#explicit_files[@]} > 0 )); then + tidy_targets=("${explicit_files[@]}") +else + tidy_targets=("${file_regex}") +fi + +set +e +PYTHONUNBUFFERED=1 run-clang-tidy \ + -p "${build_dir}" \ + -quiet \ + -j "${jobs}" \ + ${filter_args[@]+"${filter_args[@]}"} \ + ${extra_args[@]+"${extra_args[@]}"} \ + ${forward_args[@]+"${forward_args[@]}"} \ + "${tidy_targets[@]}" \ + 2>&1 | tee "${log}" +rc="${PIPESTATUS[0]}" +set -e + +if [[ "${ci_mode}" == "1" ]]; then + emit_annotations "${log}" + write_step_summary "${log}" +fi + +__TIDY_WARNINGS=0 +__TIDY_ERRORS=0 +print_stdout_summary "${log}" + +if (( __TIDY_WARNINGS > 0 || __TIDY_ERRORS > 0 )); then + echo "Results written to: $(cd "$(dirname "${log}")" && pwd)/$(basename "${log}")" +else + rm -f "${log}" +fi + +if [[ "${fail_on_warning}" == "1" && "${rc}" == "0" && "${__TIDY_WARNINGS}" -gt 0 ]]; then + echo "clang-tidy: --fail-on-warning is set and ${__TIDY_WARNINGS} warning(s) were emitted; exiting non-zero." >&2 + rc=1 +fi + +exit "${rc}" diff --git a/scripts/install-pre-commit.sh b/scripts/install-pre-commit.sh new file mode 100755 index 0000000..2422a7f --- /dev/null +++ b/scripts/install-pre-commit.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Copyright 2026 LiveKit +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: install-pre-commit.sh [--repo-root PATH] + +Install a git pre-commit hook that runs clang-format on staged C/C++ files. +The generated hook prefers a consuming repo compatibility wrapper at +./scripts/clang-format.sh and falls back to ./cpp-tools/scripts/clang-format.sh. +EOF +} + +repo_root="${REPO_ROOT:-}" +while (($#)); do + case "$1" in + -h|--help|-\?) + usage + exit 0 + ;; + --repo-root) + if (($# < 2)); then + echo "ERROR: --repo-root requires a path." >&2 + exit 2 + fi + repo_root="$2" + shift 2 + ;; + *) + echo "ERROR: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "${repo_root}" ]]; then + repo_root="$(git rev-parse --show-toplevel)" +fi +repo_root="$(cd "${repo_root}" && pwd -P)" +hook_path="${repo_root}/.git/hooks/pre-commit" + +cat >"${hook_path}" <<'HOOK' +#!/bin/sh +# Auto-format staged C/C++ files using LiveKit C++ tooling. +files=$(git diff --cached --name-only --diff-filter=ACMR \ + -- "*.c" "*.cc" "*.cpp" "*.cxx" "*.h" "*.hpp" "*.hxx") +[ -z "${files}" ] && exit 0 + +if [ -x "./scripts/clang-format.sh" ]; then + echo "${files}" | xargs ./scripts/clang-format.sh --fix +elif [ -x "./cpp-tools/scripts/clang-format.sh" ]; then + repo_root=$(git rev-parse --show-toplevel) + echo "${files}" | xargs ./cpp-tools/scripts/clang-format.sh --repo-root "${repo_root}" --fix +else + echo "ERROR: no LiveKit clang-format wrapper found." >&2 + exit 1 +fi + +echo "${files}" | xargs git add +HOOK + +chmod +x "${hook_path}" +echo "Installed pre-commit hook at ${hook_path}" From 9f3067c6e19fdbe528baf6abb7f1c2dbbb703984 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 10 Jun 2026 18:56:39 -0600 Subject: [PATCH 02/24] Add reusable C++ checks workflow Move C++ check orchestration into cpp-tools so consumers can configure clang-format, clang-tidy, and Doxygen from a shared workflow. Co-authored-by: Cursor --- .github/workflows/cpp-tools.yml | 238 ++++++++++++++++++++++++++++++++ README.md | 1 + docs/tools.md | 24 ++++ 3 files changed, 263 insertions(+) create mode 100644 .github/workflows/cpp-tools.yml diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml new file mode 100644 index 0000000..3dc4a4c --- /dev/null +++ b/.github/workflows/cpp-tools.yml @@ -0,0 +1,238 @@ +name: C++ Tools + +on: + workflow_call: + inputs: + clang_format: + type: boolean + default: true + clang_tidy: + type: boolean + default: true + doxygen: + type: boolean + default: false + checkout_submodules: + type: string + default: recursive + clang_format_version: + type: string + default: "22" + clang_format_paths: + type: string + default: "src include benchmarks" + clang_tidy_version: + type: string + default: "19" + clang_tidy_dependencies_command: + type: string + default: | + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake ninja-build pkg-config \ + llvm-dev libclang-dev clang \ + libssl-dev wget ca-certificates gnupg + clang_tidy_install_rust: + type: boolean + default: false + clang_tidy_setup_command: + type: string + default: "" + clang_tidy_configure_command: + type: string + default: "" + clang_tidy_generate_command: + type: string + default: "" + clang_tidy_build_dir: + type: string + default: build-release + clang_tidy_file_regex: + type: string + default: '^(?!.*/(_deps|build-[^/]*|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' + clang_tidy_header_filter: + type: string + default: "" + clang_tidy_exclude_header_filter: + type: string + default: "" + clang_tidy_require_generated_protobuf: + type: string + default: "" + doxygen_dependencies_command: + type: string + default: | + sudo apt-get update + sudo apt-get install -y doxygen graphviz + doxygen_command: + type: string + default: ./scripts/generate-docs.sh +permissions: + contents: read + +jobs: + clang-format: + name: clang-format + if: ${{ inputs.clang_format }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: ${{ inputs.checkout_submodules }} + fetch-depth: 1 + + - name: Install clang-format ${{ inputs.clang_format_version }} + env: + LLVM_VERSION: ${{ inputs.clang_format_version }} + run: | + set -eux + sudo install -m 0755 -d /etc/apt/keyrings + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /etc/apt/keyrings/llvm.asc >/dev/null + sudo chmod a+r /etc/apt/keyrings/llvm.asc + codename=$(lsb_release -cs) + echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${LLVM_VERSION} main" \ + | sudo tee "/etc/apt/sources.list.d/llvm-${LLVM_VERSION}.list" >/dev/null + sudo apt-get update + sudo apt-get install -y "clang-format-${LLVM_VERSION}" + sudo ln -sf "/usr/bin/clang-format-${LLVM_VERSION}" /usr/local/bin/clang-format + clang-format --version + + - name: Run clang-format + env: + FORMAT_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CLANG_FORMAT_PATHS_INPUT: ${{ inputs.clang_format_paths }} + run: | + set -euo pipefail + args=() + for path in ${CLANG_FORMAT_PATHS_INPUT}; do + args+=(--path "${path}") + done + ./cpp-tools/scripts/clang-format.sh \ + --repo-root "$GITHUB_WORKSPACE" \ + "${args[@]}" + + clang-tidy: + name: clang-tidy + if: ${{ inputs.clang_tidy }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: ${{ inputs.checkout_submodules }} + fetch-depth: 1 + + - name: Install dependencies + if: ${{ inputs.clang_tidy_dependencies_command != '' }} + env: + CLANG_TIDY_DEPENDENCIES_COMMAND: ${{ inputs.clang_tidy_dependencies_command }} + run: | + set -euo pipefail + eval "${CLANG_TIDY_DEPENDENCIES_COMMAND}" + + - name: Install clang-tidy ${{ inputs.clang_tidy_version }} + env: + LLVM_VERSION: ${{ inputs.clang_tidy_version }} + run: | + set -eux + sudo install -m 0755 -d /etc/apt/keyrings + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /etc/apt/keyrings/llvm.asc >/dev/null + sudo chmod a+r /etc/apt/keyrings/llvm.asc + codename=$(lsb_release -cs) + echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${LLVM_VERSION} main" \ + | sudo tee "/etc/apt/sources.list.d/llvm-${LLVM_VERSION}.list" >/dev/null + sudo apt-get update + sudo apt-get install -y "clang-tidy-${LLVM_VERSION}" "clang-tools-${LLVM_VERSION}" + sudo ln -sf "/usr/bin/clang-tidy-${LLVM_VERSION}" /usr/local/bin/clang-tidy + sudo ln -sf "/usr/bin/run-clang-tidy-${LLVM_VERSION}" /usr/local/bin/run-clang-tidy + clang-tidy --version + run-clang-tidy --help | head -1 || true + + - name: Install Rust + if: ${{ inputs.clang_tidy_install_rust }} + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + with: + toolchain: stable + + - name: Setup clang-tidy environment + if: ${{ inputs.clang_tidy_setup_command != '' }} + env: + CLANG_TIDY_SETUP_COMMAND: ${{ inputs.clang_tidy_setup_command }} + run: | + set -euo pipefail + eval "${CLANG_TIDY_SETUP_COMMAND}" + + - name: Configure project + if: ${{ inputs.clang_tidy_configure_command != '' }} + env: + CLANG_TIDY_CONFIGURE_COMMAND: ${{ inputs.clang_tidy_configure_command }} + run: | + set -euo pipefail + eval "${CLANG_TIDY_CONFIGURE_COMMAND}" + + - name: Generate clang-tidy prerequisites + if: ${{ inputs.clang_tidy_generate_command != '' }} + env: + CLANG_TIDY_GENERATE_COMMAND: ${{ inputs.clang_tidy_generate_command }} + run: | + set -euo pipefail + eval "${CLANG_TIDY_GENERATE_COMMAND}" + + - name: Run clang-tidy + env: + TIDY_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CLANG_TIDY_BUILD_DIR: ${{ inputs.clang_tidy_build_dir }} + CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} + CLANG_TIDY_HEADER_FILTER: ${{ inputs.clang_tidy_header_filter }} + CLANG_TIDY_EXCLUDE_HEADER_FILTER: ${{ inputs.clang_tidy_exclude_header_filter }} + CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF: ${{ inputs.clang_tidy_require_generated_protobuf }} + run: | + set -euo pipefail + args=( + --repo-root "$GITHUB_WORKSPACE" + --build-dir "${CLANG_TIDY_BUILD_DIR}" + --file-regex "${CLANG_TIDY_FILE_REGEX}" + --fail-on-warning + ) + if [[ -n "${CLANG_TIDY_HEADER_FILTER}" ]]; then + args+=(--header-filter "${CLANG_TIDY_HEADER_FILTER}") + fi + if [[ -n "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}" ]]; then + args+=(--exclude-header-filter "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}") + fi + if [[ -n "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}" ]]; then + args+=(--require-generated-protobuf "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}") + fi + ./cpp-tools/scripts/clang-tidy.sh "${args[@]}" + + doxygen: + name: doxygen + if: ${{ inputs.doxygen }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: ${{ inputs.checkout_submodules }} + fetch-depth: 1 + + - name: Install Doxygen dependencies + if: ${{ inputs.doxygen_dependencies_command != '' }} + env: + DOXYGEN_DEPENDENCIES_COMMAND: ${{ inputs.doxygen_dependencies_command }} + run: | + set -euo pipefail + eval "${DOXYGEN_DEPENDENCIES_COMMAND}" + + - name: Run Doxygen + env: + DOXYGEN_COMMAND: ${{ inputs.doxygen_command }} + run: | + set -euo pipefail + eval "${DOXYGEN_COMMAND}" diff --git a/README.md b/README.md index b9a2c70..8457d5d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ repositories. It provides the shared source of truth for: - `clang-format` style (`configs/.clang-format`) - `clang-tidy` checks (`configs/.clang-tidy`) - local and CI wrapper scripts under `scripts/` +- reusable GitHub Actions workflow under `.github/workflows/cpp-tools.yml` - documentation for the shared tooling under `docs/` Consumer repositories should expose root-level symlinks for `.clang-format` and diff --git a/docs/tools.md b/docs/tools.md index f670391..3996048 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -56,3 +56,27 @@ Install the shared auto-format hook from a consuming repository: The hook formats staged C/C++ files and re-stages any files rewritten by `clang-format`. + +## GitHub Actions + +Consumer repositories can delegate C++ checks to the shared reusable workflow: + +```yaml +jobs: + cpp-tools: + uses: livekit/cpp-tools/.github/workflows/cpp-tools.yml@main + with: + clang_format: true + clang_tidy: true + doxygen: false +``` + +Checks are configured with boolean inputs: + +- `clang_format` runs `clang-format`. +- `clang_tidy` runs `clang-tidy`. +- `doxygen` runs a configurable Doxygen command. + +Repository-specific commands and filters are supplied with string inputs such +as `clang_tidy_configure_command`, `clang_tidy_generate_command`, +`clang_tidy_file_regex`, and `clang_format_paths`. From 7d5c71d06634f7168a38c814df7eb54e51a24748 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 15 Jul 2026 17:17:18 -0600 Subject: [PATCH 03/24] Cleaner layout for easier use, updated README --- configs/.clang-format => .clang-format | 0 configs/.clang-tidy => .clang-tidy | 0 .github/workflows/cpp-tools.yml | 4 +- README.md | 109 +++++++++++++- scripts/clang-format.sh => clang-format.sh | 4 +- scripts/clang-tidy.sh => clang-tidy.sh | 0 docs/tools.md | 82 ----------- ...all-pre-commit.sh => install-pre-commit.sh | 6 +- install.sh | 136 ++++++++++++++++++ 9 files changed, 245 insertions(+), 96 deletions(-) rename configs/.clang-format => .clang-format (100%) rename configs/.clang-tidy => .clang-tidy (100%) rename scripts/clang-format.sh => clang-format.sh (98%) rename scripts/clang-tidy.sh => clang-tidy.sh (100%) delete mode 100644 docs/tools.md rename scripts/install-pre-commit.sh => install-pre-commit.sh (89%) create mode 100755 install.sh diff --git a/configs/.clang-format b/.clang-format similarity index 100% rename from configs/.clang-format rename to .clang-format diff --git a/configs/.clang-tidy b/.clang-tidy similarity index 100% rename from configs/.clang-tidy rename to .clang-tidy diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 3dc4a4c..2d8b349 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -110,7 +110,7 @@ jobs: for path in ${CLANG_FORMAT_PATHS_INPUT}; do args+=(--path "${path}") done - ./cpp-tools/scripts/clang-format.sh \ + ./cpp-tools/clang-format.sh \ --repo-root "$GITHUB_WORKSPACE" \ "${args[@]}" @@ -208,7 +208,7 @@ jobs: if [[ -n "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}" ]]; then args+=(--require-generated-protobuf "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}") fi - ./cpp-tools/scripts/clang-tidy.sh "${args[@]}" + ./cpp-tools/clang-tidy.sh "${args[@]}" doxygen: name: doxygen diff --git a/README.md b/README.md index 8457d5d..d516686 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,113 @@ # cpp-tools -Standardized tools for LiveKit C++ projects. +Standardized formatting, static-analysis, and documentation checks for LiveKit +C++ projects. This repository is intended to be consumed as a git submodule by LiveKit C++ -repositories. It provides the shared source of truth for: +repositories. -- `clang-format` style (`configs/.clang-format`) -- `clang-tidy` checks (`configs/.clang-tidy`) -- local and CI wrapper scripts under `scripts/` +## Quick start + +From the consuming repository root, install the shared configuration symlinks: + +```bash +./cpp-tools/install.sh +``` + +The installer creates `.clang-format` and `.clang-tidy` links at the consumer +root. It refuses to replace existing files unless `--force` is passed. Use +`--repo-root PATH` when the consumer root cannot be detected automatically. + +Run the tools from the same directory: + +```bash +# Check formatting or rewrite files in place. +./cpp-tools/clang-format.sh +./cpp-tools/clang-format.sh --fix + +# Run static analysis after generating compile_commands.json. +./cpp-tools/clang-tidy.sh +./cpp-tools/clang-tidy.sh --fail-on-warning +``` + +Optionally install the optional auto-format pre-commit hook, which runs the above tools automatically on commit to save CI iterations: + +```bash +./cpp-tools/install-pre-commit.sh +``` + +## What this repository provides + +The shared source of truth includes: + +- `clang-format` style (`.clang-format`) +- `clang-tidy` checks (`.clang-tidy`) +- local and CI wrapper scripts at the repository root - reusable GitHub Actions workflow under `.github/workflows/cpp-tools.yml` -- documentation for the shared tooling under `docs/` Consumer repositories should expose root-level symlinks for `.clang-format` and `.clang-tidy` so editor integrations can find them, and should pass project-specific paths/build filters to the shared scripts. -See [docs/tools.md](docs/tools.md) for usage. +## clang-format + +By default, `clang-format.sh` checks tracked C and C++ files in existing +`src/`, `include/`, and `benchmarks/` trees. Repositories with different +layouts can pass `--path` repeatedly: + +```bash +./cpp-tools/clang-format.sh \ + --path src/first_package \ + --path src/second_package +``` + +The `CLANG_FORMAT_PATHS` environment variable provides the same configuration +as a colon-separated list. Positional file paths restrict the check to those +files. + +## clang-tidy + +The consuming repository must generate `compile_commands.json` before running +`clang-tidy.sh`. Project-specific behavior is configured with command-line +flags or their corresponding environment variables: + +```bash +./cpp-tools/clang-tidy.sh \ + --build-dir build-release \ + --file-regex '.*src/.*\.(c|cpp|cc|cxx)$' \ + --header-filter '.*/(include|src)/.*\.(h|hpp)$' \ + --exclude-header-filter '(.*/tests/.*)|(.*/build-[^/]*/.*)' +``` + +Use `--require-generated-protobuf PATH` when generated protobuf headers must +exist before analysis. Additional `run-clang-tidy` arguments can be passed +after `--`. + +## Pre-commit hook + +`install-pre-commit.sh` installs `.git/hooks/pre-commit`. The hook formats +staged C and C++ files and re-stages files rewritten by `clang-format`. +Re-run the installer after cloning a fresh checkout. + +## GitHub Actions + +Consumer repositories can delegate checks to the reusable workflow: + +```yaml +jobs: + cpp-tools: + uses: livekit/cpp-tools/.github/workflows/cpp-tools.yml@main + with: + clang_format: true + clang_tidy: true + doxygen: false +``` + +The boolean inputs enable the corresponding jobs. Repository-specific commands +and filters are supplied with string inputs such as +`clang_tidy_configure_command`, `clang_tidy_generate_command`, +`clang_tidy_file_regex`, and `clang_format_paths`. + +The Doxygen job installs Doxygen and Graphviz, then runs +`./scripts/generate-docs.sh` by default. Repositories can override +`doxygen_dependencies_command` and `doxygen_command`. diff --git a/scripts/clang-format.sh b/clang-format.sh similarity index 98% rename from scripts/clang-format.sh rename to clang-format.sh index e0d3cc9..e5dc960 100755 --- a/scripts/clang-format.sh +++ b/clang-format.sh @@ -234,7 +234,7 @@ write_step_summary() { echo echo "" echo - echo "Run \`./cpp-tools/scripts/clang-format.sh --fix\` locally to apply formatting." + echo "Run \`./cpp-tools/clang-format.sh --fix\` locally to apply formatting." fi } >> "${summary_file}" @@ -272,7 +272,7 @@ print_stdout_summary() { printf ' %s\n' "${f}" done < "${files_tsv}" echo - echo " Run './cpp-tools/scripts/clang-format.sh --fix' to apply formatting." + echo " Run './cpp-tools/clang-format.sh --fix' to apply formatting." fi echo "------------------------------------------------------------" diff --git a/scripts/clang-tidy.sh b/clang-tidy.sh similarity index 100% rename from scripts/clang-tidy.sh rename to clang-tidy.sh diff --git a/docs/tools.md b/docs/tools.md deleted file mode 100644 index 3996048..0000000 --- a/docs/tools.md +++ /dev/null @@ -1,82 +0,0 @@ -# LiveKit C++ Tools - -This repository owns shared C++ formatting and static-analysis policy for -LiveKit C++ projects. - -## Configs - -Consumer repositories should expose root-level symlinks so editor extensions -and local command-line tools can discover the standard config files: - -```bash -ln -s cpp-tools/configs/.clang-format .clang-format -ln -s cpp-tools/configs/.clang-tidy .clang-tidy -``` - -## clang-format - -Run from a consuming repository root: - -```bash -./cpp-tools/scripts/clang-format.sh -./cpp-tools/scripts/clang-format.sh --fix -``` - -By default the script scans existing `src/`, `include/`, and `benchmarks/` -trees. Repos with different layouts can pass `--path` repeatedly or set -`CLANG_FORMAT_PATHS` to a colon-separated list. - -## clang-tidy - -Run after the consuming repository has generated `compile_commands.json`: - -```bash -./cpp-tools/scripts/clang-tidy.sh -./cpp-tools/scripts/clang-tidy.sh --fail-on-warning -``` - -Project-specific behavior is configured with command-line flags or environment -variables: - -```bash -./cpp-tools/scripts/clang-tidy.sh \ - --build-dir build-release \ - --file-regex '.*src/.*\.(c|cpp|cc|cxx)$' \ - --header-filter '.*/(include|src)/.*\.(h|hpp)$' \ - --exclude-header-filter '(.*/tests/.*)|(.*/build-[^/]*/.*)' -``` - -## Pre-commit Hook - -Install the shared auto-format hook from a consuming repository: - -```bash -./cpp-tools/scripts/install-pre-commit.sh -``` - -The hook formats staged C/C++ files and re-stages any files rewritten by -`clang-format`. - -## GitHub Actions - -Consumer repositories can delegate C++ checks to the shared reusable workflow: - -```yaml -jobs: - cpp-tools: - uses: livekit/cpp-tools/.github/workflows/cpp-tools.yml@main - with: - clang_format: true - clang_tidy: true - doxygen: false -``` - -Checks are configured with boolean inputs: - -- `clang_format` runs `clang-format`. -- `clang_tidy` runs `clang-tidy`. -- `doxygen` runs a configurable Doxygen command. - -Repository-specific commands and filters are supplied with string inputs such -as `clang_tidy_configure_command`, `clang_tidy_generate_command`, -`clang_tidy_file_regex`, and `clang_format_paths`. diff --git a/scripts/install-pre-commit.sh b/install-pre-commit.sh similarity index 89% rename from scripts/install-pre-commit.sh rename to install-pre-commit.sh index 2422a7f..761acd0 100755 --- a/scripts/install-pre-commit.sh +++ b/install-pre-commit.sh @@ -22,7 +22,7 @@ Usage: install-pre-commit.sh [--repo-root PATH] Install a git pre-commit hook that runs clang-format on staged C/C++ files. The generated hook prefers a consuming repo compatibility wrapper at -./scripts/clang-format.sh and falls back to ./cpp-tools/scripts/clang-format.sh. +./scripts/clang-format.sh and falls back to ./cpp-tools/clang-format.sh. EOF } @@ -64,9 +64,9 @@ files=$(git diff --cached --name-only --diff-filter=ACMR \ if [ -x "./scripts/clang-format.sh" ]; then echo "${files}" | xargs ./scripts/clang-format.sh --fix -elif [ -x "./cpp-tools/scripts/clang-format.sh" ]; then +elif [ -x "./cpp-tools/clang-format.sh" ]; then repo_root=$(git rev-parse --show-toplevel) - echo "${files}" | xargs ./cpp-tools/scripts/clang-format.sh --repo-root "${repo_root}" --fix + echo "${files}" | xargs ./cpp-tools/clang-format.sh --repo-root "${repo_root}" --fix else echo "ERROR: no LiveKit clang-format wrapper found." >&2 exit 1 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..2dcf48e --- /dev/null +++ b/install.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# +# Copyright 2026 LiveKit +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./cpp-tools/install.sh [OPTIONS] + +Install root-level symlinks for the shared clang configuration files. + +Options: + --repo-root PATH + Consumer repository root. Defaults to the cpp-tools superproject, + then the Git repository containing the current working directory. + --force + Replace existing files or symlinks at the destination paths. + -h, --help + Show this help and exit. +EOF +} + +tools_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +repo_root="" +force=0 + +while (($#)); do + case "$1" in + --repo-root) + if (($# < 2)); then + echo "ERROR: --repo-root requires a path." >&2 + exit 2 + fi + repo_root="$2" + shift 2 + ;; + --force) + force=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "${repo_root}" ]]; then + repo_root="$(git -C "${tools_root}" rev-parse --show-superproject-working-tree 2>/dev/null || true)" +fi +if [[ -z "${repo_root}" ]]; then + repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" +fi +if [[ -z "${repo_root}" ]]; then + echo "ERROR: could not determine the consumer repository root." >&2 + echo "Run this script from the consumer repository or pass --repo-root." >&2 + exit 1 +fi + +repo_root="$(cd "${repo_root}" && pwd -P)" +if [[ "${repo_root}" == "${tools_root}" ]]; then + echo "ERROR: refusing to install links into the cpp-tools repository itself." >&2 + echo "Run from a consuming repository or pass --repo-root." >&2 + exit 1 +fi + +case "${tools_root}" in + "${repo_root}"/*) + tools_relative="${tools_root#"${repo_root}"/}" + ;; + *) + echo "ERROR: cpp-tools must be located inside the consumer repository." >&2 + echo "cpp-tools: ${tools_root}" >&2 + echo "consumer: ${repo_root}" >&2 + exit 1 + ;; +esac + +configs=(.clang-format .clang-tidy) + +# Validate every destination before changing any of them. +for config in "${configs[@]}"; do + source_path="${tools_root}/${config}" + destination="${repo_root}/${config}" + link_target="${tools_relative}/${config}" + + if [[ ! -f "${source_path}" ]]; then + echo "ERROR: shared config not found: ${source_path}" >&2 + exit 1 + fi + + if [[ -L "${destination}" ]] && [[ "$(readlink "${destination}")" == "${link_target}" ]]; then + continue + fi + + if [[ -e "${destination}" || -L "${destination}" ]] && ((force == 0)); then + echo "ERROR: ${destination} already exists." >&2 + echo "Re-run with --force to replace existing config files or symlinks." >&2 + exit 1 + fi +done + +for config in "${configs[@]}"; do + destination="${repo_root}/${config}" + link_target="${tools_relative}/${config}" + + if [[ -L "${destination}" ]] && [[ "$(readlink "${destination}")" == "${link_target}" ]]; then + echo "Already installed: ${destination} -> ${link_target}" + continue + fi + + if [[ -e "${destination}" || -L "${destination}" ]]; then + rm -f "${destination}" + fi + + ln -s "${link_target}" "${destination}" + echo "Installed: ${destination} -> ${link_target}" +done From f2dbc227187f9434f6bba9e95e0a2da4638b973b Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 15 Jul 2026 17:33:46 -0600 Subject: [PATCH 04/24] Harden shared C++ checks workflow Install shared configs before checks and align workflow safeguards with consumer CI expectations. Co-authored-by: Cursor --- .github/workflows/cpp-tools.yml | 15 ++++++++++++--- install-pre-commit.sh | 7 ++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 2d8b349..544ab8c 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -75,14 +75,18 @@ jobs: name: clang-format if: ${{ inputs.clang_format }} runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: ${{ inputs.checkout_submodules }} fetch-depth: 1 + - name: Install shared configuration + run: ./cpp-tools/install.sh --repo-root "$GITHUB_WORKSPACE" --force + - name: Install clang-format ${{ inputs.clang_format_version }} env: LLVM_VERSION: ${{ inputs.clang_format_version }} @@ -118,14 +122,18 @@ jobs: name: clang-tidy if: ${{ inputs.clang_tidy }} runs-on: ubuntu-latest + timeout-minutes: 45 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: ${{ inputs.checkout_submodules }} fetch-depth: 1 + - name: Install shared configuration + run: ./cpp-tools/install.sh --repo-root "$GITHUB_WORKSPACE" --force + - name: Install dependencies if: ${{ inputs.clang_tidy_dependencies_command != '' }} env: @@ -214,10 +222,11 @@ jobs: name: doxygen if: ${{ inputs.doxygen }} runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: ${{ inputs.checkout_submodules }} fetch-depth: 1 diff --git a/install-pre-commit.sh b/install-pre-commit.sh index 761acd0..f7cbcd6 100755 --- a/install-pre-commit.sh +++ b/install-pre-commit.sh @@ -21,8 +21,7 @@ usage() { Usage: install-pre-commit.sh [--repo-root PATH] Install a git pre-commit hook that runs clang-format on staged C/C++ files. -The generated hook prefers a consuming repo compatibility wrapper at -./scripts/clang-format.sh and falls back to ./cpp-tools/clang-format.sh. +The generated hook uses ./cpp-tools/clang-format.sh. EOF } @@ -62,9 +61,7 @@ files=$(git diff --cached --name-only --diff-filter=ACMR \ -- "*.c" "*.cc" "*.cpp" "*.cxx" "*.h" "*.hpp" "*.hxx") [ -z "${files}" ] && exit 0 -if [ -x "./scripts/clang-format.sh" ]; then - echo "${files}" | xargs ./scripts/clang-format.sh --fix -elif [ -x "./cpp-tools/clang-format.sh" ]; then +if [ -x "./cpp-tools/clang-format.sh" ]; then repo_root=$(git rev-parse --show-toplevel) echo "${files}" | xargs ./cpp-tools/clang-format.sh --repo-root "${repo_root}" --fix else From 53a90dc59010e18edb7d71c54be0a2f259fe57d8 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 15 Jul 2026 19:03:05 -0600 Subject: [PATCH 05/24] Undo doxygen --- .github/workflows/cpp-tools.yml | 39 --------------------------------- README.md | 5 ----- 2 files changed, 44 deletions(-) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 544ab8c..5db83d8 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -9,9 +9,6 @@ on: clang_tidy: type: boolean default: true - doxygen: - type: boolean - default: false checkout_submodules: type: string default: recursive @@ -59,14 +56,6 @@ on: clang_tidy_require_generated_protobuf: type: string default: "" - doxygen_dependencies_command: - type: string - default: | - sudo apt-get update - sudo apt-get install -y doxygen graphviz - doxygen_command: - type: string - default: ./scripts/generate-docs.sh permissions: contents: read @@ -217,31 +206,3 @@ jobs: args+=(--require-generated-protobuf "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}") fi ./cpp-tools/clang-tidy.sh "${args[@]}" - - doxygen: - name: doxygen - if: ${{ inputs.doxygen }} - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: ${{ inputs.checkout_submodules }} - fetch-depth: 1 - - - name: Install Doxygen dependencies - if: ${{ inputs.doxygen_dependencies_command != '' }} - env: - DOXYGEN_DEPENDENCIES_COMMAND: ${{ inputs.doxygen_dependencies_command }} - run: | - set -euo pipefail - eval "${DOXYGEN_DEPENDENCIES_COMMAND}" - - - name: Run Doxygen - env: - DOXYGEN_COMMAND: ${{ inputs.doxygen_command }} - run: | - set -euo pipefail - eval "${DOXYGEN_COMMAND}" diff --git a/README.md b/README.md index d516686..6139068 100644 --- a/README.md +++ b/README.md @@ -100,14 +100,9 @@ jobs: with: clang_format: true clang_tidy: true - doxygen: false ``` The boolean inputs enable the corresponding jobs. Repository-specific commands and filters are supplied with string inputs such as `clang_tidy_configure_command`, `clang_tidy_generate_command`, `clang_tidy_file_regex`, and `clang_format_paths`. - -The Doxygen job installs Doxygen and Graphviz, then runs -`./scripts/generate-docs.sh` by default. Repositories can override -`doxygen_dependencies_command` and `doxygen_command`. From 12c945180b6a2b48149c79be79d9d7d727fc3c12 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 10:28:57 -0600 Subject: [PATCH 06/24] Cleaner install --- .github/workflows/cpp-tools.yml | 4 +- README.md | 31 ++++++------ install-pre-commit.sh | 76 ------------------------------ install.sh | 83 +++++++++++++++++++++++++++++++-- 4 files changed, 99 insertions(+), 95 deletions(-) delete mode 100755 install-pre-commit.sh diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 5db83d8..1b33fff 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -74,7 +74,7 @@ jobs: fetch-depth: 1 - name: Install shared configuration - run: ./cpp-tools/install.sh --repo-root "$GITHUB_WORKSPACE" --force + run: ./cpp-tools/install.sh clang-format --repo-root "$GITHUB_WORKSPACE" --force - name: Install clang-format ${{ inputs.clang_format_version }} env: @@ -121,7 +121,7 @@ jobs: fetch-depth: 1 - name: Install shared configuration - run: ./cpp-tools/install.sh --repo-root "$GITHUB_WORKSPACE" --force + run: ./cpp-tools/install.sh clang-tidy --repo-root "$GITHUB_WORKSPACE" --force - name: Install dependencies if: ${{ inputs.clang_tidy_dependencies_command != '' }} diff --git a/README.md b/README.md index 6139068..4d85fdb 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,26 @@ repositories. ## Quick start -From the consuming repository root, install the shared configuration symlinks: +From the consuming repository root, install the shared configuration symlinks +and pre-commit hook: ```bash ./cpp-tools/install.sh ``` -The installer creates `.clang-format` and `.clang-tidy` links at the consumer -root. It refuses to replace existing files unless `--force` is passed. Use -`--repo-root PATH` when the consumer root cannot be detected automatically. +With no component arguments, the installer creates `.clang-format` and +`.clang-tidy` links at the consumer root and installs the auto-formatting +pre-commit hook. Pass component names to install only what is needed: + +```bash +./cpp-tools/install.sh clang-tidy clang-format precommit-hook +./cpp-tools/install.sh clang-tidy +./cpp-tools/install.sh precommit-hook +``` + +The installer refuses to replace existing configuration files unless `--force` +is passed. Use `--repo-root PATH` when the consumer root cannot be detected +automatically. Run the tools from the same directory: @@ -30,12 +41,6 @@ Run the tools from the same directory: ./cpp-tools/clang-tidy.sh --fail-on-warning ``` -Optionally install the optional auto-format pre-commit hook, which runs the above tools automatically on commit to save CI iterations: - -```bash -./cpp-tools/install-pre-commit.sh -``` - ## What this repository provides The shared source of truth includes: @@ -85,9 +90,9 @@ after `--`. ## Pre-commit hook -`install-pre-commit.sh` installs `.git/hooks/pre-commit`. The hook formats -staged C and C++ files and re-stages files rewritten by `clang-format`. -Re-run the installer after cloning a fresh checkout. +`install.sh precommit-hook` installs `.git/hooks/pre-commit`. The hook formats +staged C and C++ files and re-stages files rewritten by `clang-format`. Re-run +the installer after cloning a fresh checkout. ## GitHub Actions diff --git a/install-pre-commit.sh b/install-pre-commit.sh deleted file mode 100755 index f7cbcd6..0000000 --- a/install-pre-commit.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2026 LiveKit -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: install-pre-commit.sh [--repo-root PATH] - -Install a git pre-commit hook that runs clang-format on staged C/C++ files. -The generated hook uses ./cpp-tools/clang-format.sh. -EOF -} - -repo_root="${REPO_ROOT:-}" -while (($#)); do - case "$1" in - -h|--help|-\?) - usage - exit 0 - ;; - --repo-root) - if (($# < 2)); then - echo "ERROR: --repo-root requires a path." >&2 - exit 2 - fi - repo_root="$2" - shift 2 - ;; - *) - echo "ERROR: unknown option: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -if [[ -z "${repo_root}" ]]; then - repo_root="$(git rev-parse --show-toplevel)" -fi -repo_root="$(cd "${repo_root}" && pwd -P)" -hook_path="${repo_root}/.git/hooks/pre-commit" - -cat >"${hook_path}" <<'HOOK' -#!/bin/sh -# Auto-format staged C/C++ files using LiveKit C++ tooling. -files=$(git diff --cached --name-only --diff-filter=ACMR \ - -- "*.c" "*.cc" "*.cpp" "*.cxx" "*.h" "*.hpp" "*.hxx") -[ -z "${files}" ] && exit 0 - -if [ -x "./cpp-tools/clang-format.sh" ]; then - repo_root=$(git rev-parse --show-toplevel) - echo "${files}" | xargs ./cpp-tools/clang-format.sh --repo-root "${repo_root}" --fix -else - echo "ERROR: no LiveKit clang-format wrapper found." >&2 - exit 1 -fi - -echo "${files}" | xargs git add -HOOK - -chmod +x "${hook_path}" -echo "Installed pre-commit hook at ${hook_path}" diff --git a/install.sh b/install.sh index 2dcf48e..73770c4 100755 --- a/install.sh +++ b/install.sh @@ -18,9 +18,18 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: ./cpp-tools/install.sh [OPTIONS] +Usage: ./cpp-tools/install.sh [COMPONENT...] [OPTIONS] -Install root-level symlinks for the shared clang configuration files. +Install shared clang configuration symlinks and the pre-commit hook. +With no components, installs everything. + +Components: + clang-format + Install the root-level .clang-format symlink. + clang-tidy + Install the root-level .clang-tidy symlink. + precommit-hook + Install the auto-formatting Git pre-commit hook. Options: --repo-root PATH @@ -33,12 +42,57 @@ Options: EOF } +install_precommit_hook() { + local consumer_root="$1" + local hook_path="${consumer_root}/.git/hooks/pre-commit" + + cat >"${hook_path}" <<'HOOK' +#!/bin/sh +# Auto-format staged C/C++ files using LiveKit C++ tooling. +files=$(git diff --cached --name-only --diff-filter=ACMR \ + -- "*.c" "*.cc" "*.cpp" "*.cxx" "*.h" "*.hpp" "*.hxx") +[ -z "${files}" ] && exit 0 + +if [ -x "./cpp-tools/clang-format.sh" ]; then + repo_root=$(git rev-parse --show-toplevel) + echo "${files}" | xargs ./cpp-tools/clang-format.sh --repo-root "${repo_root}" --fix +else + echo "ERROR: no LiveKit clang-format wrapper found." >&2 + exit 1 +fi + +echo "${files}" | xargs git add +HOOK + + chmod +x "${hook_path}" + echo "Installed pre-commit hook at ${hook_path}" +} + tools_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" repo_root="" force=0 +components_specified=0 +install_clang_format=0 +install_clang_tidy=0 +install_precommit=0 while (($#)); do case "$1" in + clang-format) + components_specified=1 + install_clang_format=1 + shift + ;; + clang-tidy) + components_specified=1 + install_clang_tidy=1 + shift + ;; + precommit-hook) + components_specified=1 + install_precommit=1 + shift + ;; --repo-root) if (($# < 2)); then echo "ERROR: --repo-root requires a path." >&2 @@ -55,14 +109,25 @@ while (($#)); do usage exit 0 ;; - *) + -*) echo "ERROR: unknown option: $1" >&2 usage >&2 exit 2 ;; + *) + echo "ERROR: unknown component: $1" >&2 + usage >&2 + exit 2 + ;; esac done +if ((components_specified == 0)); then + install_clang_format=1 + install_clang_tidy=1 + install_precommit=1 +fi + if [[ -z "${repo_root}" ]]; then repo_root="$(git -C "${tools_root}" rev-parse --show-superproject-working-tree 2>/dev/null || true)" fi @@ -94,7 +159,13 @@ case "${tools_root}" in ;; esac -configs=(.clang-format .clang-tidy) +configs=() +if ((install_clang_format == 1)); then + configs+=(.clang-format) +fi +if ((install_clang_tidy == 1)); then + configs+=(.clang-tidy) +fi # Validate every destination before changing any of them. for config in "${configs[@]}"; do @@ -134,3 +205,7 @@ for config in "${configs[@]}"; do ln -s "${link_target}" "${destination}" echo "Installed: ${destination} -> ${link_target}" done + +if ((install_precommit == 1)); then + install_precommit_hook "${repo_root}" +fi From cdfd74449b54e91c69454c82061f92242e42dfb2 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 13:34:02 -0600 Subject: [PATCH 07/24] Don't make pre-commit hook required --- README.md | 14 +++++++------- install.sh | 5 ++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4d85fdb..78c76c5 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,15 @@ repositories. ## Quick start -From the consuming repository root, install the shared configuration symlinks -and pre-commit hook: +From the consuming repository root, install the shared configuration symlinks: ```bash ./cpp-tools/install.sh ``` With no component arguments, the installer creates `.clang-format` and -`.clang-tidy` links at the consumer root and installs the auto-formatting -pre-commit hook. Pass component names to install only what is needed: +`.clang-tidy` links at the consumer root. Pass component names to install only +what is needed or to explicitly opt into the pre-commit hook: ```bash ./cpp-tools/install.sh clang-tidy clang-format precommit-hook @@ -90,9 +89,10 @@ after `--`. ## Pre-commit hook -`install.sh precommit-hook` installs `.git/hooks/pre-commit`. The hook formats -staged C and C++ files and re-stages files rewritten by `clang-format`. Re-run -the installer after cloning a fresh checkout. +The hook is not installed by default. `install.sh precommit-hook` explicitly +installs `.git/hooks/pre-commit`. The hook formats staged C and C++ files and +re-stages files rewritten by `clang-format`. Re-run the installer after cloning +a fresh checkout. ## GitHub Actions diff --git a/install.sh b/install.sh index 73770c4..4474cbc 100755 --- a/install.sh +++ b/install.sh @@ -20,8 +20,8 @@ usage() { cat <<'EOF' Usage: ./cpp-tools/install.sh [COMPONENT...] [OPTIONS] -Install shared clang configuration symlinks and the pre-commit hook. -With no components, installs everything. +Install shared clang configuration symlinks and, when requested, the +pre-commit hook. With no components, installs both configuration symlinks. Components: clang-format @@ -125,7 +125,6 @@ done if ((components_specified == 0)); then install_clang_format=1 install_clang_tidy=1 - install_precommit=1 fi if [[ -z "${repo_root}" ]]; then From 257865db67c5ff4e093538af4e574e346721c6f2 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 15:06:51 -0600 Subject: [PATCH 08/24] Separate AGENTS.md --- AGENTS.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ 2 files changed, 113 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..15fa451 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,109 @@ +# AGENTS.md — Shared C++ Engineering Baseline + +## Scope + +These rules apply to LiveKit C++ projects that consume `cpp-tools`. A consuming +repository's `AGENTS.md` should define its architecture, supported platforms, +build commands, and project-specific exceptions. More specific repository rules +take precedence when they explicitly conflict with this baseline. + +## Safety and Determinism + +- Design for predictable memory use, execution time, and failure behavior. +- Avoid heap allocation when practical. In real-time, callback, and steady-state + paths, allocate resources during initialization and reuse them. +- Prefer stack storage, RAII, fixed-capacity storage, and bounded pools or + queues. Document unavoidable dynamic allocation in time-sensitive code. +- Put explicit bounds on queues, retries, buffers, worker counts, and waits. + Define observable behavior for exhaustion and overload. +- Do not block real-time or callback threads with file/network I/O, sleeps, + unbounded work, allocation, or contended locks. Offload work through bounded + mechanisms with clear back-pressure. +- Avoid unbounded recursion and large stack objects. Account for stack limits on + embedded targets. + +## Errors and Failure Modes + +- Use return values for expected failures instead of exceptions: + - `std::optional` when absence is expected and needs no diagnostic. + - `bool` for a simple success/failure result. + - A project `Result` or equivalent when callers need a typed error. +- Callers must inspect status-bearing return values. Mark important results + `[[nodiscard]]` where practical. +- Do not throw through C, FFI, callback, destructor, real-time, or + resource-constrained boundaries. +- Reserve exceptions for genuinely exceptional failures when the consuming + project permits them. Catch them at a well-defined boundary and convert them + to the project's error model. +- Validate external inputs and cross-boundary data. Keep state valid on failure + and prefer fail-safe behavior over partial updates. + +## Memory, Ownership, and Lifetime + +- Make ownership explicit. Prefer values and RAII types; do not use raw owning + pointers. +- Use `std::unique_ptr` for exclusive dynamic ownership and `std::shared_ptr` + only when ownership is genuinely shared. +- Keep object lifetimes and teardown order deterministic. Destructors must not + throw. +- Avoid hidden copies of large buffers. Make copy and move behavior intentional, + especially for media, sensor, and message data. +- Declare data at the smallest useful scope and initialize it before use. + +## Types, Arithmetic, and Units + +- Prefer fixed-width integers from `` when width or signedness matters, + including serialization, FFI, hardware, timestamps, IDs, and public APIs. +- Use platform-sized primitive integers only when the value is intentionally + platform-sized or compatibility requires it. +- Avoid implicit narrowing and mixed signed/unsigned arithmetic. Validate ranges + before conversions and arithmetic that can overflow. +- Represent durations and time points with `std::chrono`; use a monotonic clock + for elapsed time, deadlines, and timeouts. +- Make physical units explicit with strong or clearly named types. Do not pass + ambiguous raw numeric values across interfaces. + +## Concurrency + +- Document which threads call an API and whether each type is thread-safe. +- Minimize shared mutable state. Protect it with clear synchronization and keep + critical sections short. +- Never call user code while holding an internal lock. +- Use bounded waits and define cancellation and shutdown behavior. Join worker + threads outside locks. +- Treat atomics and lock-free code as specialized tools; document memory-order + reasoning and test concurrency paths under stress. + +## Design and Readability + +- Keep functions focused and short, roughly 60 lines or fewer when practical. +- Prefer straightforward control flow over clever abstractions. Document any + deliberate tradeoff between readability, determinism, and performance. +- Use `enum class`, `nullptr`, explicit conversions, and const-correct + interfaces. +- Check non-void return values and make ignored results explicit. +- Use `git mv` when moving or renaming tracked files. + +## Portability + +- Avoid undefined behavior, compiler-specific assumptions, and dependence on + host endianness, alignment, or primitive widths. +- Keep cross-platform and cross-architecture boundaries explicit. Test all + supported targets defined by the consuming repository. +- Keep third-party implementation details out of public headers and ABI + boundaries. + +## Verification + +- Adhere to the shared `.clang-format` and `.clang-tidy` configurations. +- After C++ changes, run `./cpp-tools/clang-format.sh` with the consuming + project's paths. Use `--fix` when needed, then rerun the check. +- Generate the consuming project's compilation database and run + `./cpp-tools/clang-tidy.sh` with its documented build directory and filters. +- Do not bypass formatter or static-analysis failures. Keep suppressions narrow, + local, and justified in code. +- Add deterministic tests for normal, boundary, overload, timeout, cancellation, + and failure behavior. Avoid timing-only sleeps when a condition or simulated + clock can be used. +- Benchmark or stress-test new time-sensitive or resource-sensitive behavior and + verify that configured limits are enforced. diff --git a/README.md b/README.md index 78c76c5..9621f7b 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The shared source of truth includes: - `clang-format` style (`.clang-format`) - `clang-tidy` checks (`.clang-tidy`) +- shared C++ engineering guidance (`AGENTS.md`) - local and CI wrapper scripts at the repository root - reusable GitHub Actions workflow under `.github/workflows/cpp-tools.yml` @@ -53,6 +54,9 @@ Consumer repositories should expose root-level symlinks for `.clang-format` and `.clang-tidy` so editor integrations can find them, and should pass project-specific paths/build filters to the shared scripts. +Their root `AGENTS.md` should reference `cpp-tools/AGENTS.md` as the shared C++ +baseline and add only repository-specific architecture and workflow guidance. + ## clang-format By default, `clang-format.sh` checks tracked C and C++ files in existing From ce85debfc27940821f36f60d0bc3617ab46a3189 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 15:11:07 -0600 Subject: [PATCH 09/24] New section on clangd --- AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 15fa451..ec822ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,15 @@ take precedence when they explicitly conflict with this baseline. - Keep third-party implementation details out of public headers and ABI boundaries. +## Project-Owned clangd Configuration + +- Each consuming project must provide its own `.clangd`; compilation database + locations and flags are project-specific and are not shared by `cpp-tools`. +- Verify `.clangd` points clangd at the project's generated compilation database + before relying on IDE diagnostics. +- clang-tidy does not read `.clangd`. Before running clang-tidy, generate a valid + `compile_commands.json` and pass its build directory to `clang-tidy.sh`. + ## Verification - Adhere to the shared `.clang-format` and `.clang-tidy` configurations. From 0f2e3f451e66325937fdea6b79226bab5ef8be02 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 15:22:16 -0600 Subject: [PATCH 10/24] Another agents md update --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ec822ed..c175b7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,8 @@ take precedence when they explicitly conflict with this baseline. pointers. - Use `std::unique_ptr` for exclusive dynamic ownership and `std::shared_ptr` only when ownership is genuinely shared. +- Avoid heap allocations as much as possible. If code is heap allocating in a loop + or a high-frequency path, second guess the design and consider alternatives. - Keep object lifetimes and teardown order deterministic. Destructors must not throw. - Avoid hidden copies of large buffers. Make copy and move behavior intentional, From e9468a36926eeea3776ece839aa762392e26eed3 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 16:25:26 -0600 Subject: [PATCH 11/24] AGENTS.md adjustments --- AGENTS.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c175b7f..0c7e756 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,10 +2,8 @@ ## Scope -These rules apply to LiveKit C++ projects that consume `cpp-tools`. A consuming -repository's `AGENTS.md` should define its architecture, supported platforms, -build commands, and project-specific exceptions. More specific repository rules -take precedence when they explicitly conflict with this baseline. +These rules apply to LiveKit C++ projects that consume `cpp-tools`. If a consuming +repository has an `AGENTS.md` with conflicting rules they should take priority. ## Safety and Determinism @@ -27,7 +25,7 @@ take precedence when they explicitly conflict with this baseline. - Use return values for expected failures instead of exceptions: - `std::optional` when absence is expected and needs no diagnostic. - `bool` for a simple success/failure result. - - A project `Result` or equivalent when callers need a typed error. + - `Result`, `expected` (C++23 or higher), or equivalent when callers need a typed error. - Callers must inspect status-bearing return values. Mark important results `[[nodiscard]]` where practical. - Do not throw through C, FFI, callback, destructor, real-time, or @@ -54,6 +52,7 @@ take precedence when they explicitly conflict with this baseline. ## Types, Arithmetic, and Units +- Prefer STL types over third-party dependencies when possible. - Prefer fixed-width integers from `` when width or signedness matters, including serialization, FFI, hardware, timestamps, IDs, and public APIs. - Use platform-sized primitive integers only when the value is intentionally @@ -62,8 +61,8 @@ take precedence when they explicitly conflict with this baseline. before conversions and arithmetic that can overflow. - Represent durations and time points with `std::chrono`; use a monotonic clock for elapsed time, deadlines, and timeouts. -- Make physical units explicit with strong or clearly named types. Do not pass - ambiguous raw numeric values across interfaces. +- Make physical units explicit with strong or clearly named types, such as `_us` + for microseconds. Do not pass ambiguous raw numeric values across interfaces. ## Concurrency From 9b5b95a254f3181a57d7a9454c44cafd89835ce0 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 18:11:35 -0600 Subject: [PATCH 12/24] More inputs/descriptions --- .github/workflows/cpp-tools.yml | 27 +++++++++++++++++++++++++-- README.md | 32 ++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 1b33fff..3d937d2 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -4,24 +4,35 @@ on: workflow_call: inputs: clang_format: + description: Run the clang-format job. type: boolean default: true clang_tidy: + description: Run the clang-tidy job. type: boolean default: true checkout_submodules: + description: Submodule checkout mode passed to actions/checkout. type: string default: recursive clang_format_version: + description: LLVM clang-format major version to install. type: string default: "22" clang_format_paths: + description: Space-separated repository paths checked by clang-format. type: string default: "src include benchmarks" clang_tidy_version: + description: LLVM clang-tidy major version to install. type: string default: "19" + clang_tidy_fail_on_warning: + description: Fail the clang-tidy job when warnings are emitted. + type: boolean + default: true clang_tidy_dependencies_command: + description: Shell command that installs project dependencies for clang-tidy. type: string default: | sudo apt-get update @@ -30,30 +41,39 @@ on: llvm-dev libclang-dev clang \ libssl-dev wget ca-certificates gnupg clang_tidy_install_rust: + description: Install the stable Rust toolchain before configuring the project. type: boolean default: false clang_tidy_setup_command: + description: Optional shell command that prepares the clang-tidy environment. type: string default: "" clang_tidy_configure_command: + description: Shell command that configures the compilation database build. type: string default: "" clang_tidy_generate_command: + description: Optional shell command that generates required source files. type: string default: "" clang_tidy_build_dir: + description: Directory containing compile_commands.json. type: string default: build-release clang_tidy_file_regex: + description: Regular expression selecting source files for run-clang-tidy. type: string - default: '^(?!.*/(_deps|build-[^/]*|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' + required: true clang_tidy_header_filter: + description: Optional clang-tidy header filter regular expression. type: string default: "" clang_tidy_exclude_header_filter: + description: Optional clang-tidy excluded-header filter regular expression. type: string default: "" clang_tidy_require_generated_protobuf: + description: Optional directory that must contain generated protobuf headers. type: string default: "" permissions: @@ -188,14 +208,17 @@ jobs: CLANG_TIDY_HEADER_FILTER: ${{ inputs.clang_tidy_header_filter }} CLANG_TIDY_EXCLUDE_HEADER_FILTER: ${{ inputs.clang_tidy_exclude_header_filter }} CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF: ${{ inputs.clang_tidy_require_generated_protobuf }} + CLANG_TIDY_FAIL_ON_WARNING: ${{ inputs.clang_tidy_fail_on_warning }} run: | set -euo pipefail args=( --repo-root "$GITHUB_WORKSPACE" --build-dir "${CLANG_TIDY_BUILD_DIR}" --file-regex "${CLANG_TIDY_FILE_REGEX}" - --fail-on-warning ) + if [[ "${CLANG_TIDY_FAIL_ON_WARNING}" == "true" ]]; then + args+=(--fail-on-warning) + fi if [[ -n "${CLANG_TIDY_HEADER_FILTER}" ]]; then args+=(--header-filter "${CLANG_TIDY_HEADER_FILTER}") fi diff --git a/README.md b/README.md index 9621f7b..5ee26d6 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,32 @@ repositories. ## Quick start +Add `cpp-tools` to the consuming repository: + +```bash +git submodule add https://github.com/livekit/cpp-tools.git cpp-tools +``` + From the consuming repository root, install the shared configuration symlinks: ```bash -./cpp-tools/install.sh +./cpp-tools/install.sh # Installs .clang-format and .clang-tidy symlinks to repo root ``` -With no component arguments, the installer creates `.clang-format` and -`.clang-tidy` links at the consumer root. Pass component names to install only -what is needed or to explicitly opt into the pre-commit hook: +Optionally pass `precommit-hook` to install a precommit hook that automatically runs `clang-format` +before commits. + +> Note: Only one precommit hook is allowed in Git ```bash -./cpp-tools/install.sh clang-tidy clang-format precommit-hook -./cpp-tools/install.sh clang-tidy -./cpp-tools/install.sh precommit-hook +./cpp-tools/install.sh precommit-hook # Installs precommit hook ``` The installer refuses to replace existing configuration files unless `--force` is passed. Use `--repo-root PATH` when the consumer root cannot be detected automatically. -Run the tools from the same directory: +Run the tools from the repository root: ```bash # Check formatting or rewrite files in place. @@ -109,9 +114,12 @@ jobs: with: clang_format: true clang_tidy: true + clang_tidy_fail_on_warning: true + clang_tidy_file_regex: '.*\.(c|cpp|cc|cxx)$' ``` -The boolean inputs enable the corresponding jobs. Repository-specific commands -and filters are supplied with string inputs such as -`clang_tidy_configure_command`, `clang_tidy_generate_command`, -`clang_tidy_file_regex`, and `clang_format_paths`. +The workflow documents each supported input alongside its default. +`clang_tidy_file_regex` is required because source layouts and exclusions are +consumer-specific. Other repository-specific commands and filters are supplied +with inputs such as `clang_tidy_configure_command`, +`clang_tidy_generate_command`, and `clang_format_paths`. From d0934bbfb57ae927ccb83a3a830059407fe7055c Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 18:14:58 -0600 Subject: [PATCH 13/24] Regex not required --- .github/workflows/cpp-tools.yml | 13 +++++++++++-- README.md | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 3d937d2..5163d52 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -61,9 +61,9 @@ on: type: string default: build-release clang_tidy_file_regex: - description: Regular expression selecting source files for run-clang-tidy. + description: Regular expression selecting source files for run-clang-tidy; required when clang_tidy is enabled. type: string - required: true + default: "" clang_tidy_header_filter: description: Optional clang-tidy header filter regular expression. type: string @@ -134,6 +134,15 @@ jobs: timeout-minutes: 45 steps: + - name: Validate clang-tidy inputs + env: + CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} + run: | + if [[ -z "${CLANG_TIDY_FILE_REGEX}" ]]; then + echo "::error::clang_tidy_file_regex is required when clang_tidy is enabled." + exit 2 + fi + - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: diff --git a/README.md b/README.md index 5ee26d6..4ab8d45 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ jobs: ``` The workflow documents each supported input alongside its default. -`clang_tidy_file_regex` is required because source layouts and exclusions are -consumer-specific. Other repository-specific commands and filters are supplied +`clang_tidy_file_regex` is required when `clang_tidy` is enabled because source +layouts and exclusions are consumer-specific. Format-only consumers do not +need to set it. Other repository-specific commands and filters are supplied with inputs such as `clang_tidy_configure_command`, `clang_tidy_generate_command`, and `clang_format_paths`. From 28286583b59ab4f7d2690ac4748f749783344d41 Mon Sep 17 00:00:00 2001 From: Alan George Date: Wed, 15 Jul 2026 19:19:19 -0600 Subject: [PATCH 14/24] Fix clang-format directory scans Rely on extension filtering after Git file discovery so directory-based checks do not exclude every tracked file. Co-authored-by: Cursor --- clang-format.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/clang-format.sh b/clang-format.sh index e5dc960..4e7b542 100755 --- a/clang-format.sh +++ b/clang-format.sh @@ -139,10 +139,6 @@ else files+=("${path}") done < <(git ls-files -z -- \ "${paths[@]}" \ - ':!*.md' \ - ':!*.txt' \ - ':!*.cmake' \ - ':!CMakeLists.txt' \ | while IFS= read -r -d '' path; do case "${path}" in *.c|*.cc|*.cpp|*.cxx|*.h|*.hpp|*.hxx) printf '%s\0' "${path}" ;; From 707762771f079e0932c8c6a123e943a4b2fefe32 Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 21:43:52 -0600 Subject: [PATCH 15/24] Add consumer tooling runners Keep shared scripts repository-agnostic while allowing each project to use the same project-aware entrypoints locally and in CI. Co-authored-by: Cursor --- .github/workflows/cpp-tools.yml | 101 ++++++++++++++++++++++---------- README.md | 64 +++++++++++++------- clang-format.sh | 37 ++++++++---- clang-tidy.sh | 35 +++-------- 4 files changed, 147 insertions(+), 90 deletions(-) diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml index 5163d52..772466b 100644 --- a/.github/workflows/cpp-tools.yml +++ b/.github/workflows/cpp-tools.yml @@ -19,14 +19,22 @@ on: description: LLVM clang-format major version to install. type: string default: "22" + clang_format_runner: + description: Optional executable consumer wrapper; when set, clang_format_paths is not required. + type: string + default: "" clang_format_paths: - description: Space-separated repository paths checked by clang-format. + description: Space-separated paths checked by clang-format; required when enabled without a runner. type: string - default: "src include benchmarks" + default: "" clang_tidy_version: description: LLVM clang-tidy major version to install. type: string default: "19" + clang_tidy_runner: + description: Optional executable consumer wrapper; when set, clang_tidy_file_regex is not required. + type: string + default: "" clang_tidy_fail_on_warning: description: Fail the clang-tidy job when warnings are emitted. type: boolean @@ -61,7 +69,7 @@ on: type: string default: build-release clang_tidy_file_regex: - description: Regular expression selecting source files for run-clang-tidy; required when clang_tidy is enabled. + description: Source-file regex for run-clang-tidy; required when enabled without a runner. type: string default: "" clang_tidy_header_filter: @@ -72,10 +80,6 @@ on: description: Optional clang-tidy excluded-header filter regular expression. type: string default: "" - clang_tidy_require_generated_protobuf: - description: Optional directory that must contain generated protobuf headers. - type: string - default: "" permissions: contents: read @@ -87,6 +91,16 @@ jobs: timeout-minutes: 15 steps: + - name: Validate clang-format inputs + env: + CLANG_FORMAT_RUNNER: ${{ inputs.clang_format_runner }} + CLANG_FORMAT_PATHS_INPUT: ${{ inputs.clang_format_paths }} + run: | + if [[ -z "${CLANG_FORMAT_RUNNER}" && -z "${CLANG_FORMAT_PATHS_INPUT}" ]]; then + echo "::error::clang_format_runner or clang_format_paths is required when clang_format is enabled." + exit 2 + fi + - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -116,16 +130,29 @@ jobs: - name: Run clang-format env: FORMAT_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CLANG_FORMAT_RUNNER: ${{ inputs.clang_format_runner }} CLANG_FORMAT_PATHS_INPUT: ${{ inputs.clang_format_paths }} run: | set -euo pipefail - args=() - for path in ${CLANG_FORMAT_PATHS_INPUT}; do - args+=(--path "${path}") - done - ./cpp-tools/clang-format.sh \ - --repo-root "$GITHUB_WORKSPACE" \ - "${args[@]}" + if [[ -n "${CLANG_FORMAT_RUNNER}" ]]; then + runner="${CLANG_FORMAT_RUNNER}" + if [[ "${runner}" != /* ]]; then + runner="${GITHUB_WORKSPACE}/${runner}" + fi + if [[ ! -x "${runner}" ]]; then + echo "::error::clang_format_runner is not executable: ${CLANG_FORMAT_RUNNER}" + exit 2 + fi + "${runner}" + else + args=() + for path in ${CLANG_FORMAT_PATHS_INPUT}; do + args+=(--path "${path}") + done + ./cpp-tools/clang-format.sh \ + --repo-root "$GITHUB_WORKSPACE" \ + "${args[@]}" + fi clang-tidy: name: clang-tidy @@ -136,10 +163,11 @@ jobs: steps: - name: Validate clang-tidy inputs env: + CLANG_TIDY_RUNNER: ${{ inputs.clang_tidy_runner }} CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} run: | - if [[ -z "${CLANG_TIDY_FILE_REGEX}" ]]; then - echo "::error::clang_tidy_file_regex is required when clang_tidy is enabled." + if [[ -z "${CLANG_TIDY_RUNNER}" && -z "${CLANG_TIDY_FILE_REGEX}" ]]; then + echo "::error::clang_tidy_runner or clang_tidy_file_regex is required when clang_tidy is enabled." exit 2 fi @@ -212,29 +240,40 @@ jobs: - name: Run clang-tidy env: TIDY_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CLANG_TIDY_RUNNER: ${{ inputs.clang_tidy_runner }} CLANG_TIDY_BUILD_DIR: ${{ inputs.clang_tidy_build_dir }} CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} CLANG_TIDY_HEADER_FILTER: ${{ inputs.clang_tidy_header_filter }} CLANG_TIDY_EXCLUDE_HEADER_FILTER: ${{ inputs.clang_tidy_exclude_header_filter }} - CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF: ${{ inputs.clang_tidy_require_generated_protobuf }} CLANG_TIDY_FAIL_ON_WARNING: ${{ inputs.clang_tidy_fail_on_warning }} run: | set -euo pipefail - args=( - --repo-root "$GITHUB_WORKSPACE" - --build-dir "${CLANG_TIDY_BUILD_DIR}" - --file-regex "${CLANG_TIDY_FILE_REGEX}" - ) + args=() if [[ "${CLANG_TIDY_FAIL_ON_WARNING}" == "true" ]]; then args+=(--fail-on-warning) fi - if [[ -n "${CLANG_TIDY_HEADER_FILTER}" ]]; then - args+=(--header-filter "${CLANG_TIDY_HEADER_FILTER}") - fi - if [[ -n "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}" ]]; then - args+=(--exclude-header-filter "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}") - fi - if [[ -n "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}" ]]; then - args+=(--require-generated-protobuf "${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF}") + if [[ -n "${CLANG_TIDY_RUNNER}" ]]; then + runner="${CLANG_TIDY_RUNNER}" + if [[ "${runner}" != /* ]]; then + runner="${GITHUB_WORKSPACE}/${runner}" + fi + if [[ ! -x "${runner}" ]]; then + echo "::error::clang_tidy_runner is not executable: ${CLANG_TIDY_RUNNER}" + exit 2 + fi + "${runner}" "${args[@]}" + else + args=( + --repo-root "$GITHUB_WORKSPACE" + --build-dir "${CLANG_TIDY_BUILD_DIR}" + --file-regex "${CLANG_TIDY_FILE_REGEX}" + "${args[@]}" + ) + if [[ -n "${CLANG_TIDY_HEADER_FILTER}" ]]; then + args+=(--header-filter "${CLANG_TIDY_HEADER_FILTER}") + fi + if [[ -n "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}" ]]; then + args+=(--exclude-header-filter "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}") + fi + ./cpp-tools/clang-tidy.sh "${args[@]}" fi - ./cpp-tools/clang-tidy.sh "${args[@]}" diff --git a/README.md b/README.md index 4ab8d45..ca36be1 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,12 @@ Run the tools from the repository root: ```bash # Check formatting or rewrite files in place. -./cpp-tools/clang-format.sh -./cpp-tools/clang-format.sh --fix +./cpp-tools/clang-format.sh --path path/to/sources +./cpp-tools/clang-format.sh --path path/to/sources --fix # Run static analysis after generating compile_commands.json. -./cpp-tools/clang-tidy.sh -./cpp-tools/clang-tidy.sh --fail-on-warning +./cpp-tools/clang-tidy.sh --file-regex '.*\.(c|cpp|cc|cxx)$' +./cpp-tools/clang-tidy.sh --file-regex '.*\.(c|cpp|cc|cxx)$' --fail-on-warning ``` ## What this repository provides @@ -62,16 +62,42 @@ project-specific paths/build filters to the shared scripts. Their root `AGENTS.md` should reference `cpp-tools/AGENTS.md` as the shared C++ baseline and add only repository-specific architecture and workflow guidance. +## Consumer wrappers + +Consumer repositories should provide thin project-owned entrypoints such as +`scripts/clang-format.sh` and `scripts/clang-tidy.sh`. The wrappers encode the +repository's paths, filters, and build directory, then `exec` the shared script. +This gives developers a zero-argument command without copying the formatting, +diagnostic, or GitHub summary implementation. + +For example: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +export CLANG_FORMAT_FIX_COMMAND="./scripts/clang-format.sh --fix" +exec "${repo_root}/cpp-tools/clang-format.sh" \ + --repo-root "${repo_root}" \ + --path src \ + --path include \ + "$@" +``` + +The shared workflow can invoke the same entrypoints through +`clang_format_runner` and `clang_tidy_runner`. Direct script arguments and +workflow inputs remain available for repositories that do not use wrappers. + ## clang-format -By default, `clang-format.sh` checks tracked C and C++ files in existing -`src/`, `include/`, and `benchmarks/` trees. Repositories with different -layouts can pass `--path` repeatedly: +Each consumer supplies the tracked paths that `clang-format.sh` should check. +Pass `--path` repeatedly for repositories with multiple source trees: ```bash ./cpp-tools/clang-format.sh \ - --path src/first_package \ - --path src/second_package + --path path/to/sources \ + --path path/to/headers ``` The `CLANG_FORMAT_PATHS` environment variable provides the same configuration @@ -87,14 +113,10 @@ flags or their corresponding environment variables: ```bash ./cpp-tools/clang-tidy.sh \ --build-dir build-release \ - --file-regex '.*src/.*\.(c|cpp|cc|cxx)$' \ - --header-filter '.*/(include|src)/.*\.(h|hpp)$' \ - --exclude-header-filter '(.*/tests/.*)|(.*/build-[^/]*/.*)' + --file-regex '.*\.(c|cpp|cc|cxx)$' ``` -Use `--require-generated-protobuf PATH` when generated protobuf headers must -exist before analysis. Additional `run-clang-tidy` arguments can be passed -after `--`. +Additional `run-clang-tidy` arguments can be passed after `--`. ## Pre-commit hook @@ -113,14 +135,14 @@ jobs: uses: livekit/cpp-tools/.github/workflows/cpp-tools.yml@main with: clang_format: true + clang_format_runner: scripts/clang-format.sh clang_tidy: true clang_tidy_fail_on_warning: true - clang_tidy_file_regex: '.*\.(c|cpp|cc|cxx)$' + clang_tidy_runner: scripts/clang-tidy.sh ``` The workflow documents each supported input alongside its default. -`clang_tidy_file_regex` is required when `clang_tidy` is enabled because source -layouts and exclusions are consumer-specific. Format-only consumers do not -need to set it. Other repository-specific commands and filters are supplied -with inputs such as `clang_tidy_configure_command`, -`clang_tidy_generate_command`, and `clang_format_paths`. +Each enabled job requires either its consumer runner or its direct selection +input (`clang_format_paths` or `clang_tidy_file_regex`). Other +repository-specific setup is supplied with inputs such as +`clang_tidy_configure_command` and `clang_tidy_generate_command`. diff --git a/clang-format.sh b/clang-format.sh index 4e7b542..c725e9d 100755 --- a/clang-format.sh +++ b/clang-format.sh @@ -31,7 +31,7 @@ Options: `git rev-parse --show-toplevel` from the current directory. --path PATH Tracked path or glob to scan when FILE... is omitted. May be repeated. - Defaults to existing src/, include/, and benchmarks/ trees. + Required unless FILE... or CLANG_FORMAT_PATHS is supplied. --fix, -i Apply formatting in place. --github-actions, --gh @@ -42,6 +42,9 @@ Environment: Same as --repo-root. CLANG_FORMAT_PATHS Colon-separated list used when --path is not supplied. + CLANG_FORMAT_FIX_COMMAND + Local fix command shown in summaries. Defaults to a command matching + the paths or files supplied to this invocation. EOF } @@ -119,21 +122,14 @@ clang-format --version if (( ${#paths[@]} == 0 )) && [[ -n "${CLANG_FORMAT_PATHS:-}" ]]; then IFS=':' read -r -a paths <<< "${CLANG_FORMAT_PATHS}" fi -if (( ${#paths[@]} == 0 )); then - for candidate in src include benchmarks; do - if [[ -e "${candidate}" ]]; then - paths+=("${candidate}") - fi - done -fi files=() if (( ${#explicit_files[@]} > 0 )); then files=("${explicit_files[@]}") else if (( ${#paths[@]} == 0 )); then - echo "clang-format: no paths to scan." - exit 0 + echo "ERROR: provide at least one --path, FILE, or CLANG_FORMAT_PATHS value." >&2 + exit 2 fi while IFS= read -r -d '' path; do files+=("${path}") @@ -152,6 +148,23 @@ if (( file_count == 0 )); then exit 0 fi +fix_command="${CLANG_FORMAT_FIX_COMMAND:-}" +if [[ -z "${fix_command}" ]]; then + fix_command="./cpp-tools/clang-format.sh --fix" + fix_targets=() + if (( ${#explicit_files[@]} > 0 )); then + fix_targets=("${explicit_files[@]}") + else + for path in "${paths[@]}"; do + fix_targets+=(--path "${path}") + done + fi + for target in "${fix_targets[@]}"; do + printf -v quoted_target '%q' "${target}" + fix_command+=" ${quoted_target}" + done +fi + log="clang-format.log" : > "${log}" @@ -230,7 +243,7 @@ write_step_summary() { echo echo "" echo - echo "Run \`./cpp-tools/clang-format.sh --fix\` locally to apply formatting." + echo "Run \`${fix_command}\` locally to apply formatting." fi } >> "${summary_file}" @@ -268,7 +281,7 @@ print_stdout_summary() { printf ' %s\n' "${f}" done < "${files_tsv}" echo - echo " Run './cpp-tools/clang-format.sh --fix' to apply formatting." + echo " Run '${fix_command}' to apply formatting." fi echo "------------------------------------------------------------" diff --git a/clang-tidy.sh b/clang-tidy.sh index 55cb8f5..5e3822d 100755 --- a/clang-tidy.sh +++ b/clang-tidy.sh @@ -33,15 +33,12 @@ Options: Build directory containing compile_commands.json. Default: CLANG_TIDY_BUILD_DIR or build-release. --file-regex REGEX - run-clang-tidy target regex used when FILE... is omitted. Default: - CLANG_TIDY_FILE_REGEX or all non-test src/*.c/cpp/cc/cxx files. + run-clang-tidy target regex used when FILE... is omitted. Required + unless CLANG_TIDY_FILE_REGEX is set. --header-filter REGEX Forwarded to run-clang-tidy as -header-filter. --exclude-header-filter REGEX Forwarded to run-clang-tidy as -exclude-header-filter. - --require-generated-protobuf PATH - Require PATH to contain *.pb.h before running. May be relative to - the repo root. Useful for protobuf-backed SDKs. --fix Apply fixes in place (forwarded to run-clang-tidy as -fix). --github-actions, --gh @@ -51,19 +48,16 @@ Options: Environment: REPO_ROOT, CLANG_TIDY_BUILD_DIR, CLANG_TIDY_FILE_REGEX, - CLANG_TIDY_HEADER_FILTER, CLANG_TIDY_EXCLUDE_HEADER_FILTER, - CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF + CLANG_TIDY_HEADER_FILTER, CLANG_TIDY_EXCLUDE_HEADER_FILTER Environment equivalents for the matching options above. EOF } repo_root="${REPO_ROOT:-}" build_dir="${CLANG_TIDY_BUILD_DIR:-build-release}" -default_file_regex='^(?!.*/(_deps|build-[^/]*|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' -file_regex="${CLANG_TIDY_FILE_REGEX:-${default_file_regex}}" +file_regex="${CLANG_TIDY_FILE_REGEX:-}" header_filter="${CLANG_TIDY_HEADER_FILTER:-}" exclude_header_filter="${CLANG_TIDY_EXCLUDE_HEADER_FILTER:-}" -required_proto_dir="${CLANG_TIDY_REQUIRE_GENERATED_PROTOBUF:-}" ci_mode=0 fail_on_warning=0 forward_args=() @@ -119,14 +113,6 @@ while (($#)); do exclude_header_filter="$2" shift 2 ;; - --require-generated-protobuf) - if (($# < 2)); then - echo "ERROR: --require-generated-protobuf requires a path." >&2 - exit 2 - fi - required_proto_dir="$2" - shift 2 - ;; --fix) forward_args+=("-fix") shift @@ -164,6 +150,11 @@ while (($#)); do esac done +if (( ${#explicit_files[@]} == 0 )) && [[ -z "${file_regex}" ]]; then + echo "ERROR: provide --file-regex, FILE, or CLANG_TIDY_FILE_REGEX." >&2 + exit 2 +fi + if [[ -z "${repo_root}" ]]; then repo_root="$(git rev-parse --show-toplevel)" fi @@ -176,14 +167,6 @@ if [[ ! -f "${build_dir}/compile_commands.json" ]]; then exit 1 fi -if [[ -n "${required_proto_dir}" ]]; then - if [[ ! -d "${required_proto_dir}" ]] || ! compgen -G "${required_proto_dir}/*.pb.h" >/dev/null; then - echo "ERROR: no generated protobuf headers found in ${required_proto_dir}/." >&2 - echo "Run the consuming repository's protobuf/header generation step first." >&2 - exit 1 - fi -fi - if ! command -v run-clang-tidy >/dev/null 2>&1; then echo "ERROR: run-clang-tidy not found in PATH." >&2 echo "Install LLVM: brew install llvm (macOS)" >&2 From 1e2d2cdbb4f685be80af673d89a6e101689bf49e Mon Sep 17 00:00:00 2001 From: Alan George Date: Thu, 16 Jul 2026 22:30:53 -0600 Subject: [PATCH 16/24] Remove shared workflow --- .github/workflows/cpp-tools.yml | 279 -------------------------------- README.md | 37 +++-- 2 files changed, 18 insertions(+), 298 deletions(-) delete mode 100644 .github/workflows/cpp-tools.yml diff --git a/.github/workflows/cpp-tools.yml b/.github/workflows/cpp-tools.yml deleted file mode 100644 index 772466b..0000000 --- a/.github/workflows/cpp-tools.yml +++ /dev/null @@ -1,279 +0,0 @@ -name: C++ Tools - -on: - workflow_call: - inputs: - clang_format: - description: Run the clang-format job. - type: boolean - default: true - clang_tidy: - description: Run the clang-tidy job. - type: boolean - default: true - checkout_submodules: - description: Submodule checkout mode passed to actions/checkout. - type: string - default: recursive - clang_format_version: - description: LLVM clang-format major version to install. - type: string - default: "22" - clang_format_runner: - description: Optional executable consumer wrapper; when set, clang_format_paths is not required. - type: string - default: "" - clang_format_paths: - description: Space-separated paths checked by clang-format; required when enabled without a runner. - type: string - default: "" - clang_tidy_version: - description: LLVM clang-tidy major version to install. - type: string - default: "19" - clang_tidy_runner: - description: Optional executable consumer wrapper; when set, clang_tidy_file_regex is not required. - type: string - default: "" - clang_tidy_fail_on_warning: - description: Fail the clang-tidy job when warnings are emitted. - type: boolean - default: true - clang_tidy_dependencies_command: - description: Shell command that installs project dependencies for clang-tidy. - type: string - default: | - sudo apt-get update - sudo apt-get install -y \ - build-essential cmake ninja-build pkg-config \ - llvm-dev libclang-dev clang \ - libssl-dev wget ca-certificates gnupg - clang_tidy_install_rust: - description: Install the stable Rust toolchain before configuring the project. - type: boolean - default: false - clang_tidy_setup_command: - description: Optional shell command that prepares the clang-tidy environment. - type: string - default: "" - clang_tidy_configure_command: - description: Shell command that configures the compilation database build. - type: string - default: "" - clang_tidy_generate_command: - description: Optional shell command that generates required source files. - type: string - default: "" - clang_tidy_build_dir: - description: Directory containing compile_commands.json. - type: string - default: build-release - clang_tidy_file_regex: - description: Source-file regex for run-clang-tidy; required when enabled without a runner. - type: string - default: "" - clang_tidy_header_filter: - description: Optional clang-tidy header filter regular expression. - type: string - default: "" - clang_tidy_exclude_header_filter: - description: Optional clang-tidy excluded-header filter regular expression. - type: string - default: "" -permissions: - contents: read - -jobs: - clang-format: - name: clang-format - if: ${{ inputs.clang_format }} - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Validate clang-format inputs - env: - CLANG_FORMAT_RUNNER: ${{ inputs.clang_format_runner }} - CLANG_FORMAT_PATHS_INPUT: ${{ inputs.clang_format_paths }} - run: | - if [[ -z "${CLANG_FORMAT_RUNNER}" && -z "${CLANG_FORMAT_PATHS_INPUT}" ]]; then - echo "::error::clang_format_runner or clang_format_paths is required when clang_format is enabled." - exit 2 - fi - - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: ${{ inputs.checkout_submodules }} - fetch-depth: 1 - - - name: Install shared configuration - run: ./cpp-tools/install.sh clang-format --repo-root "$GITHUB_WORKSPACE" --force - - - name: Install clang-format ${{ inputs.clang_format_version }} - env: - LLVM_VERSION: ${{ inputs.clang_format_version }} - run: | - set -eux - sudo install -m 0755 -d /etc/apt/keyrings - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ - | sudo tee /etc/apt/keyrings/llvm.asc >/dev/null - sudo chmod a+r /etc/apt/keyrings/llvm.asc - codename=$(lsb_release -cs) - echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${LLVM_VERSION} main" \ - | sudo tee "/etc/apt/sources.list.d/llvm-${LLVM_VERSION}.list" >/dev/null - sudo apt-get update - sudo apt-get install -y "clang-format-${LLVM_VERSION}" - sudo ln -sf "/usr/bin/clang-format-${LLVM_VERSION}" /usr/local/bin/clang-format - clang-format --version - - - name: Run clang-format - env: - FORMAT_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - CLANG_FORMAT_RUNNER: ${{ inputs.clang_format_runner }} - CLANG_FORMAT_PATHS_INPUT: ${{ inputs.clang_format_paths }} - run: | - set -euo pipefail - if [[ -n "${CLANG_FORMAT_RUNNER}" ]]; then - runner="${CLANG_FORMAT_RUNNER}" - if [[ "${runner}" != /* ]]; then - runner="${GITHUB_WORKSPACE}/${runner}" - fi - if [[ ! -x "${runner}" ]]; then - echo "::error::clang_format_runner is not executable: ${CLANG_FORMAT_RUNNER}" - exit 2 - fi - "${runner}" - else - args=() - for path in ${CLANG_FORMAT_PATHS_INPUT}; do - args+=(--path "${path}") - done - ./cpp-tools/clang-format.sh \ - --repo-root "$GITHUB_WORKSPACE" \ - "${args[@]}" - fi - - clang-tidy: - name: clang-tidy - if: ${{ inputs.clang_tidy }} - runs-on: ubuntu-latest - timeout-minutes: 45 - - steps: - - name: Validate clang-tidy inputs - env: - CLANG_TIDY_RUNNER: ${{ inputs.clang_tidy_runner }} - CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} - run: | - if [[ -z "${CLANG_TIDY_RUNNER}" && -z "${CLANG_TIDY_FILE_REGEX}" ]]; then - echo "::error::clang_tidy_runner or clang_tidy_file_regex is required when clang_tidy is enabled." - exit 2 - fi - - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: ${{ inputs.checkout_submodules }} - fetch-depth: 1 - - - name: Install shared configuration - run: ./cpp-tools/install.sh clang-tidy --repo-root "$GITHUB_WORKSPACE" --force - - - name: Install dependencies - if: ${{ inputs.clang_tidy_dependencies_command != '' }} - env: - CLANG_TIDY_DEPENDENCIES_COMMAND: ${{ inputs.clang_tidy_dependencies_command }} - run: | - set -euo pipefail - eval "${CLANG_TIDY_DEPENDENCIES_COMMAND}" - - - name: Install clang-tidy ${{ inputs.clang_tidy_version }} - env: - LLVM_VERSION: ${{ inputs.clang_tidy_version }} - run: | - set -eux - sudo install -m 0755 -d /etc/apt/keyrings - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ - | sudo tee /etc/apt/keyrings/llvm.asc >/dev/null - sudo chmod a+r /etc/apt/keyrings/llvm.asc - codename=$(lsb_release -cs) - echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] http://apt.llvm.org/${codename}/ llvm-toolchain-${codename}-${LLVM_VERSION} main" \ - | sudo tee "/etc/apt/sources.list.d/llvm-${LLVM_VERSION}.list" >/dev/null - sudo apt-get update - sudo apt-get install -y "clang-tidy-${LLVM_VERSION}" "clang-tools-${LLVM_VERSION}" - sudo ln -sf "/usr/bin/clang-tidy-${LLVM_VERSION}" /usr/local/bin/clang-tidy - sudo ln -sf "/usr/bin/run-clang-tidy-${LLVM_VERSION}" /usr/local/bin/run-clang-tidy - clang-tidy --version - run-clang-tidy --help | head -1 || true - - - name: Install Rust - if: ${{ inputs.clang_tidy_install_rust }} - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 - with: - toolchain: stable - - - name: Setup clang-tidy environment - if: ${{ inputs.clang_tidy_setup_command != '' }} - env: - CLANG_TIDY_SETUP_COMMAND: ${{ inputs.clang_tidy_setup_command }} - run: | - set -euo pipefail - eval "${CLANG_TIDY_SETUP_COMMAND}" - - - name: Configure project - if: ${{ inputs.clang_tidy_configure_command != '' }} - env: - CLANG_TIDY_CONFIGURE_COMMAND: ${{ inputs.clang_tidy_configure_command }} - run: | - set -euo pipefail - eval "${CLANG_TIDY_CONFIGURE_COMMAND}" - - - name: Generate clang-tidy prerequisites - if: ${{ inputs.clang_tidy_generate_command != '' }} - env: - CLANG_TIDY_GENERATE_COMMAND: ${{ inputs.clang_tidy_generate_command }} - run: | - set -euo pipefail - eval "${CLANG_TIDY_GENERATE_COMMAND}" - - - name: Run clang-tidy - env: - TIDY_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - CLANG_TIDY_RUNNER: ${{ inputs.clang_tidy_runner }} - CLANG_TIDY_BUILD_DIR: ${{ inputs.clang_tidy_build_dir }} - CLANG_TIDY_FILE_REGEX: ${{ inputs.clang_tidy_file_regex }} - CLANG_TIDY_HEADER_FILTER: ${{ inputs.clang_tidy_header_filter }} - CLANG_TIDY_EXCLUDE_HEADER_FILTER: ${{ inputs.clang_tidy_exclude_header_filter }} - CLANG_TIDY_FAIL_ON_WARNING: ${{ inputs.clang_tidy_fail_on_warning }} - run: | - set -euo pipefail - args=() - if [[ "${CLANG_TIDY_FAIL_ON_WARNING}" == "true" ]]; then - args+=(--fail-on-warning) - fi - if [[ -n "${CLANG_TIDY_RUNNER}" ]]; then - runner="${CLANG_TIDY_RUNNER}" - if [[ "${runner}" != /* ]]; then - runner="${GITHUB_WORKSPACE}/${runner}" - fi - if [[ ! -x "${runner}" ]]; then - echo "::error::clang_tidy_runner is not executable: ${CLANG_TIDY_RUNNER}" - exit 2 - fi - "${runner}" "${args[@]}" - else - args=( - --repo-root "$GITHUB_WORKSPACE" - --build-dir "${CLANG_TIDY_BUILD_DIR}" - --file-regex "${CLANG_TIDY_FILE_REGEX}" - "${args[@]}" - ) - if [[ -n "${CLANG_TIDY_HEADER_FILTER}" ]]; then - args+=(--header-filter "${CLANG_TIDY_HEADER_FILTER}") - fi - if [[ -n "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}" ]]; then - args+=(--exclude-header-filter "${CLANG_TIDY_EXCLUDE_HEADER_FILTER}") - fi - ./cpp-tools/clang-tidy.sh "${args[@]}" - fi diff --git a/README.md b/README.md index ca36be1..4e32cd4 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ The shared source of truth includes: - `clang-tidy` checks (`.clang-tidy`) - shared C++ engineering guidance (`AGENTS.md`) - local and CI wrapper scripts at the repository root -- reusable GitHub Actions workflow under `.github/workflows/cpp-tools.yml` Consumer repositories should expose root-level symlinks for `.clang-format` and `.clang-tidy` so editor integrations can find them, and should pass @@ -85,9 +84,9 @@ exec "${repo_root}/cpp-tools/clang-format.sh" \ "$@" ``` -The shared workflow can invoke the same entrypoints through -`clang_format_runner` and `clang_tidy_runner`. Direct script arguments and -workflow inputs remain available for repositories that do not use wrappers. +Repository-owned CI workflows should invoke the same project entrypoints so +local and CI file selection cannot drift. Repositories that do not use wrappers +can call the shared scripts with explicit arguments. ## clang-format @@ -127,22 +126,22 @@ a fresh checkout. ## GitHub Actions -Consumer repositories can delegate checks to the reusable workflow: +The scripts automatically enable GitHub annotations and step summaries when +`GITHUB_ACTIONS=true`. Consumer repositories own checkout, tool installation, +and project-specific build preparation, then call their project wrappers: ```yaml -jobs: - cpp-tools: - uses: livekit/cpp-tools/.github/workflows/cpp-tools.yml@main - with: - clang_format: true - clang_format_runner: scripts/clang-format.sh - clang_tidy: true - clang_tidy_fail_on_warning: true - clang_tidy_runner: scripts/clang-tidy.sh +- name: Run clang-format + env: + FORMAT_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: ./scripts/clang-format.sh + +- name: Run clang-tidy + env: + TIDY_BLOB_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: ./scripts/clang-tidy.sh --fail-on-warning ``` -The workflow documents each supported input alongside its default. -Each enabled job requires either its consumer runner or its direct selection -input (`clang_format_paths` or `clang_tidy_file_regex`). Other -repository-specific setup is supplied with inputs such as -`clang_tidy_configure_command` and `clang_tidy_generate_command`. +`FORMAT_BLOB_SHA` and `TIDY_BLOB_SHA` make source links target the pull +request's head commit. The scripts fall back to `GITHUB_SHA` when these values +are not supplied. From 493e1a06441085fdf6d2c92f20c370c99605daf6 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 08:58:59 -0600 Subject: [PATCH 17/24] Add clang-tidy doc --- README.md | 7 ++-- docs/clang-tidy.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 docs/clang-tidy.md diff --git a/README.md b/README.md index 4e32cd4..1bcc883 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ files. ## clang-tidy +See [docs/clang-tidy.md](docs/clang-tidy.md) for the enabled checks, +exclusions, and the reasoning behind them. + The consuming repository must generate `compile_commands.json` before running `clang-tidy.sh`. Project-specific behavior is configured with command-line flags or their corresponding environment variables: @@ -143,5 +146,5 @@ and project-specific build preparation, then call their project wrappers: ``` `FORMAT_BLOB_SHA` and `TIDY_BLOB_SHA` make source links target the pull -request's head commit. The scripts fall back to `GITHUB_SHA` when these values -are not supplied. +request's head commit such that links to violating files render correctly. +The scripts fall back to `GITHUB_SHA` when these values are not supplied. diff --git a/docs/clang-tidy.md b/docs/clang-tidy.md new file mode 100644 index 0000000..c67f7c0 --- /dev/null +++ b/docs/clang-tidy.md @@ -0,0 +1,84 @@ +# clang-tidy checks + +LiveKit C++ projects use `clang-tidy` to detect correctness, safety, +maintainability, and performance issues that ordinary compiler diagnostics may +not catch. These checks are particularly valuable at FFI boundaries and in +media, robotics, and embedded code, where undefined behavior, unsafe +conversions, and lifetime mistakes can be difficult to diagnose. + +The configuration is defined at [`.clang-tidy`](../.clang-tidy). This page explains +which checks are enabled, which checks are explicitly excluded, and the reasoning. + +A pattern ending in `-*` enables every check in that category unless a later +entry excludes it. A leading `-` disables a check. See the +[complete clang-tidy check list](https://clang.llvm.org/extra/clang-tidy/checks/list.html) +for all available checks. + +## Enabled Checks + +| Check | Reasoning | +| --- | --- | +| `bugprone-*` | Catches common C++ bugs: use-after-move, dangling references, implicit conversions, incorrect loop logic, etc. | +| `performance-*` | Flags unnecessary copies, inefficient container operations, and missed move opportunities. | +| `modernize-*` | Encourages modern C++17 idioms: `auto`, range-for, `override`, `nullptr`, smart pointers. | +| `readability-misleading-indentation` | Catches indentation that suggests a different control flow than what actually executes. | +| `readability-redundant-smartptr-get` | Flags `ptr.get()` calls where `*ptr` or `ptr->` would suffice – reduces noise and makes smart pointer usage idiomatic. | +| `readability-identifier-naming` | Enforces consistent class and method naming conventions across LiveKit C++ projects. | +| `misc-const-correctness` | Similar to Rust, ensures variables are immutable by default unless intended to be changed. | + +## Excluded Checks + +| Check | Reasoning | +| --- | --- | +| `-*` (base) | Start from a clean slate rather than enabling all checks (explicitly opt-in to desired checks) | +| `-readability-braces-around-statements` | Style preference (enforces `{ }` around statements. Arguably a good thing but open to debate. | +| `-modernize-use-trailing-return-type` | Would rewrite every function to `auto foo() -> int` style. Opinionated. | +| `-modernize-avoid-c-arrays` | C-style arrays are sometimes necessary for FFI boundaries (interfacing with the Rust bridge and C APIs like `livekit_ffi`). Flagging them all would create false positives at interop boundaries. | +| `-modernize-use-auto` | Doesn’t add much value. | +| `-modernize-use-nodiscard` | Adding `[[nodiscard]]` everywhere is noisy and better done intentionally by the developer on APIs where ignoring the return value is genuinely a bug. Not clear but may be a breaking API change for downstream users that are discarding return results today. | +| `-modernize-use-auto` | Flags every case that could use `auto` that doesn’t. Opinionated/low value. | +| `-bugprone-easily-swappable-parameters` | Fires on any function with consecutive parameters of the same type (e.g., `(int width, int height)`). Extremely noisy for a codebase with many similarly-typed parameters in media/audio APIs. | +| `-performance-enum-size` | Fairly noisy and low impact performance-wise. Possibly good to turn on in the future. | +| `-modernize-return-braced-init-list` | Decided against it during review. | +| `-modernize-type-traits` | Lots of existing type traits code not using this style, could be breaking change. | + +## Warnings Treated as Errors + +The following findings are promoted to errors because they generally indicate +correctness or safety defects. In GitHub Actions, the shared scripts surface +findings as annotations and in the step summary. Consumer workflows may also +use `--fail-on-warning` to reject any remaining warning. + +| Check | Reasoning | +| --- | --- | +| `clang-analyzer-*` | Static analyzer findings (null dereference, memory leaks, dead stores) are almost always real bugs. Failing the build on these prevents them from merging. | +| `bugprone-use-after-move` | Accessing an object after `std::move` is undefined behavior. Critical in a codebase that passes ownership of SDK handles and shared pointers. | +| `bugprone-dangling-handle` | Detects references/views to temporaries that go out of scope. Especially relevant with `std::string_view` and callback patterns used throughout the SDK. | +| `bugprone-infinite-loop` | Catches loops whose condition can never become false. A hard hang in a real-time media SDK is worse than a crash. | +| `bugprone-narrowing-conversions` | Implicit narrowing (e.g., `int64_t` to `int32_t`) can silently corrupt audio frame sizes, sample rates, or timestamps. | +| `bugprone-undefined-memory-manipulation` | Flags `memset`/`memcpy` on non-trivially-copyable types. The SDK copies raw audio/video frame buffers and this ensures those operations are safe. | +| `bugprone-move-forwarding-reference` | Calling `std::move` on a forwarding reference instead of `std::forward` breaks perfect forwarding. Subtle and hard to catch in review. | +| `bugprone-incorrect-roundings` | Detects `(x + 0.5)` cast-to-int patterns that fail for negative numbers. Relevant for audio sample math and timestamp calculations. | +| `bugprone-sizeof-expression` | Catches `sizeof(ptr)` when `sizeof(*ptr)` was intended – common source of buffer size bugs in code that handles raw media buffers. | +| `bugprone-string-literal-with-embedded-nul` | A `\0` inside a string literal silently truncates it. Would cause hard-to-debug issues in protocol payloads or RPC method names. | +| `bugprone-suspicious-memset-usage` | Catches `memset(ptr, size, 0)` where arguments are swapped. Would silently corrupt audio frame buffers. | + +## Formatting + +These settings keep `clang-tidy` fixes and identifier naming consistent with the +shared LiveKit C++ style. + +| Setting | Reasoning | +| --- | --- | +| `FormatStyle: file` | Uses the project’s `.clang-format` for any auto-fix formatting, keeping fixes consistent with the existing code style. | +| `readability-identifier-naming.ClassCase: CamelCase` | Requires class names to use `CamelCase`. | +| `readability-identifier-naming.MethodCase: camelBack` | Requires method names to use `camelBack`. | + +## Other Settings + +These options control tool-wide behavior or individual checks. + +| Setting | Reasoning | +| --- | --- | +| `HeaderFilterRegex` | Only lint headers owned by this project (`include/livekit`, `src`, `bridge`, `examples`). Avoids flagging third-party/system headers. | +| `modernize-use-nullptr.NullMacros: NULL` | Tells the nullptr modernizer to also replace `NULL` macros (common in C-interop code from the FFI bridge). | From e85feaf5ef3e417f00132b1d43fc44ce483a8e88 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 09:35:29 -0600 Subject: [PATCH 18/24] README updates --- README.md | 47 ++++++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 1bcc883..7b18a88 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,34 @@ # cpp-tools -Standardized formatting, static-analysis, and documentation checks for LiveKit -C++ projects. +Standardized tools for LiveKit C++ projects. This repository provides: -This repository is intended to be consumed as a git submodule by LiveKit C++ -repositories. +- [clang-format](https://clang.llvm.org/docs/ClangFormat.html): Code styling consistency across projects +- [clang-tidy](https://clang.llvm.org/extra/clang-tidy/): Static analysis and bug catching +- Base [AGENTS.md](./AGENTS.md) with C++ best practices +- Helper scripts and GitHub actions reporting support + +This repository is intended to be consumed as a git submodule. ## Quick start -Add `cpp-tools` to the consuming repository: +Add this repository as a submodule from the consuming repository root: ```bash git submodule add https://github.com/livekit/cpp-tools.git cpp-tools ``` -From the consuming repository root, install the shared configuration symlinks: +Install the shared configuration symlinks: ```bash ./cpp-tools/install.sh # Installs .clang-format and .clang-tidy symlinks to repo root ``` -Optionally pass `precommit-hook` to install a precommit hook that automatically runs `clang-format` -before commits. - -> Note: Only one precommit hook is allowed in Git +Optionally install a precommit hook that automatically runs `clang-format` before commits: ```bash ./cpp-tools/install.sh precommit-hook # Installs precommit hook ``` -The installer refuses to replace existing configuration files unless `--force` -is passed. Use `--repo-root PATH` when the consumer root cannot be detected -automatically. - Run the tools from the repository root: ```bash @@ -45,23 +41,16 @@ Run the tools from the repository root: ./cpp-tools/clang-tidy.sh --file-regex '.*\.(c|cpp|cc|cxx)$' --fail-on-warning ``` -## What this repository provides - -The shared source of truth includes: +Update existing `AGENTS.md` file to reference this one: -- `clang-format` style (`.clang-format`) -- `clang-tidy` checks (`.clang-tidy`) -- shared C++ engineering guidance (`AGENTS.md`) -- local and CI wrapper scripts at the repository root - -Consumer repositories should expose root-level symlinks for `.clang-format` and -`.clang-tidy` so editor integrations can find them, and should pass -project-specific paths/build filters to the shared scripts. - -Their root `AGENTS.md` should reference `cpp-tools/AGENTS.md` as the shared C++ -baseline and add only repository-specific architecture and workflow guidance. +```markdown +## Shared C++ baseline +Follow `cpp-tools/AGENTS.md` for shared LiveKit C++ engineering guidance. +Instructions in this file are project-specific and take precedence if they +conflict with the shared baseline. +``` -## Consumer wrappers +## Tool wrappers Consumer repositories should provide thin project-owned entrypoints such as `scripts/clang-format.sh` and `scripts/clang-tidy.sh`. The wrappers encode the From 41bf0d3379b63038cdc18abcdee3ffed67c28938 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 10:17:13 -0600 Subject: [PATCH 19/24] Update README --- README.md | 63 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 7b18a88..e0e6abd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Standardized tools for LiveKit C++ projects. This repository provides: - [clang-format](https://clang.llvm.org/docs/ClangFormat.html): Code styling consistency across projects -- [clang-tidy](https://clang.llvm.org/extra/clang-tidy/): Static analysis and bug catching +- [clang-tidy](https://clang.llvm.org/extra/clang-tidy/): Standardized static analysis and bug catching checks. See [docs/clang-tidy.md](./docs/clang-tidy.md) - Base [AGENTS.md](./AGENTS.md) with C++ best practices - Helper scripts and GitHub actions reporting support @@ -41,6 +41,8 @@ Run the tools from the repository root: ./cpp-tools/clang-tidy.sh --file-regex '.*\.(c|cpp|cc|cxx)$' --fail-on-warning ``` +> Note: It's recommended to wrap these tool scripts in consuming repository scripts such that arguments and options can be passed in and re-used in CI. See below section for examples. + Update existing `AGENTS.md` file to reference this one: ```markdown @@ -50,33 +52,6 @@ Instructions in this file are project-specific and take precedence if they conflict with the shared baseline. ``` -## Tool wrappers - -Consumer repositories should provide thin project-owned entrypoints such as -`scripts/clang-format.sh` and `scripts/clang-tidy.sh`. The wrappers encode the -repository's paths, filters, and build directory, then `exec` the shared script. -This gives developers a zero-argument command without copying the formatting, -diagnostic, or GitHub summary implementation. - -For example: - -```bash -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" -export CLANG_FORMAT_FIX_COMMAND="./scripts/clang-format.sh --fix" -exec "${repo_root}/cpp-tools/clang-format.sh" \ - --repo-root "${repo_root}" \ - --path src \ - --path include \ - "$@" -``` - -Repository-owned CI workflows should invoke the same project entrypoints so -local and CI file selection cannot drift. Repositories that do not use wrappers -can call the shared scripts with explicit arguments. - ## clang-format Each consumer supplies the tracked paths that `clang-format.sh` should check. @@ -111,10 +86,34 @@ Additional `run-clang-tidy` arguments can be passed after `--`. ## Pre-commit hook -The hook is not installed by default. `install.sh precommit-hook` explicitly -installs `.git/hooks/pre-commit`. The hook formats staged C and C++ files and -re-stages files rewritten by `clang-format`. Re-run the installer after cloning -a fresh checkout. +The hook formats staged C++ files and re-stages files rewritten by `clang-format`. + +## Tool wrappers + +Consuming repositories should provide thin project-owned entrypoints such as +`scripts/clang-format.sh` and `scripts/clang-tidy.sh`. The wrappers encode the +repository's paths, filters, and build directory, then `exec` the shared script. +This gives developers a zero-argument command without copying the formatting, +diagnostic, or GitHub summary implementation. + +For example: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +export CLANG_FORMAT_FIX_COMMAND="./scripts/clang-format.sh --fix" +exec "${repo_root}/cpp-tools/clang-format.sh" \ + --repo-root "${repo_root}" \ + --path src \ + --path include \ + "$@" +``` + +Repository-owned CI workflows should invoke the same project entrypoints so +local and CI file selection cannot drift. Repositories that do not use wrappers +can call the shared scripts with explicit arguments. ## GitHub Actions From ebb47ffa38d36d889034dd96592d3b95bcc02140 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 10:18:18 -0600 Subject: [PATCH 20/24] Another README update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e0e6abd..4e28fc4 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,6 @@ flags or their corresponding environment variables: Additional `run-clang-tidy` arguments can be passed after `--`. -## Pre-commit hook - -The hook formats staged C++ files and re-stages files rewritten by `clang-format`. - ## Tool wrappers Consuming repositories should provide thin project-owned entrypoints such as @@ -115,6 +111,10 @@ Repository-owned CI workflows should invoke the same project entrypoints so local and CI file selection cannot drift. Repositories that do not use wrappers can call the shared scripts with explicit arguments. +## Pre-commit hook + +The hook formats staged C++ files and re-stages files rewritten by `clang-format`. + ## GitHub Actions The scripts automatically enable GitHub annotations and step summaries when From 2cc7268c4ef565e2e4273f84dd17284a4c17ff56 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 10:22:52 -0600 Subject: [PATCH 21/24] Add CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..53bee0a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @alan-george-lk @stephen-derosa \ No newline at end of file From 846464b922300e4319527446db21f0a3a2d9ff4a Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 10:33:24 -0600 Subject: [PATCH 22/24] Basic CI job --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + .markdownlint-cli2.jsonc | 5 ++++ clang-format.sh | 2 +- clang-tidy.sh | 8 ++--- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .markdownlint-cli2.jsonc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3aaceda --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + push: + branches: ["main"] + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install ShellCheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Check shell scripts + run: | + bash -n ./*.sh + shellcheck ./*.sh + + - name: Check Markdown + run: npx --yes markdownlint-cli2@0.23.1 README.md AGENTS.md 'docs/**/*.md' + + link-check: + name: Link Check + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Restore lychee cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + - name: Run lychee + uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 + with: + args: >- + --verbose + --no-progress + --cache + --max-cache-age 1d + --root-dir . + README.md + AGENTS.md + 'docs/**/*.md' + fail: true + jobSummary: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c4c7e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.lycheecache diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..2485208 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,5 @@ +{ + "config": { + "MD013": false + } +} diff --git a/clang-format.sh b/clang-format.sh index c725e9d..02d7d7b 100755 --- a/clang-format.sh +++ b/clang-format.sh @@ -179,7 +179,7 @@ emit_annotations() { lineno="${BASH_REMATCH[2]}" col="${BASH_REMATCH[3]}" message="${BASH_REMATCH[5]}" - rel_path="${path#${workspace}/}" + rel_path="${path#"${workspace}"/}" message="${message//$'%'/%25}" message="${message//$'\r'/%0D}" message="${message//$'\n'/%0A}" diff --git a/clang-tidy.sh b/clang-tidy.sh index 5e3822d..849a7c2 100755 --- a/clang-tidy.sh +++ b/clang-tidy.sh @@ -210,7 +210,7 @@ emit_annotations() { message="${BASH_REMATCH[5]}" check="${BASH_REMATCH[6]}" check="${check%,-warnings-as-errors}" - rel_path="${path#${workspace}/}" + rel_path="${path#"${workspace}"/}" message="${message//$'%'/%25}" message="${message//$'\r'/%0D}" message="${message//$'\n'/%0A}" @@ -225,13 +225,13 @@ check_link() { local rest="${name#*-}" case "${name}" in clang-diagnostic-*) - printf '`%s`' "${name}" + printf "\`%s\`" "${name}" ;; clang-analyzer-*) - printf '[`%s`](https://clang.llvm.org/docs/analyzer/checkers.html)' "${name}" + printf "[\`%s\`](https://clang.llvm.org/docs/analyzer/checkers.html)" "${name}" ;; *) - printf '[`%s`](https://clang.llvm.org/extra/clang-tidy/checks/%s/%s.html)' \ + printf "[\`%s\`](https://clang.llvm.org/extra/clang-tidy/checks/%s/%s.html)" \ "${name}" "${module}" "${rest}" ;; esac From 05ce3e81763814d10dd20300e8d3c06f54e5393b Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 12:34:28 -0600 Subject: [PATCH 23/24] Actually use analyzer --- .clang-tidy | 1 + docs/clang-tidy.md | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index d332d0e..925a8bd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,6 @@ Checks: > -*, + clang-analyzer-*, bugprone-*, misc-const-correctness, performance-*, diff --git a/docs/clang-tidy.md b/docs/clang-tidy.md index c67f7c0..2643faa 100644 --- a/docs/clang-tidy.md +++ b/docs/clang-tidy.md @@ -18,6 +18,7 @@ for all available checks. | Check | Reasoning | | --- | --- | +| `clang-analyzer-*` | Enables Clang static analyzer checks for issues such as null dereferences, memory leaks, and dead stores. | | `bugprone-*` | Catches common C++ bugs: use-after-move, dangling references, implicit conversions, incorrect loop logic, etc. | | `performance-*` | Flags unnecessary copies, inefficient container operations, and missed move opportunities. | | `modernize-*` | Encourages modern C++17 idioms: `auto`, range-for, `override`, `nullptr`, smart pointers. | @@ -34,9 +35,8 @@ for all available checks. | `-readability-braces-around-statements` | Style preference (enforces `{ }` around statements. Arguably a good thing but open to debate. | | `-modernize-use-trailing-return-type` | Would rewrite every function to `auto foo() -> int` style. Opinionated. | | `-modernize-avoid-c-arrays` | C-style arrays are sometimes necessary for FFI boundaries (interfacing with the Rust bridge and C APIs like `livekit_ffi`). Flagging them all would create false positives at interop boundaries. | -| `-modernize-use-auto` | Doesn’t add much value. | +| `-modernize-use-auto` | Flags every case that could use `auto`. This is opinionated and does not add enough value to justify the resulting noise. | | `-modernize-use-nodiscard` | Adding `[[nodiscard]]` everywhere is noisy and better done intentionally by the developer on APIs where ignoring the return value is genuinely a bug. Not clear but may be a breaking API change for downstream users that are discarding return results today. | -| `-modernize-use-auto` | Flags every case that could use `auto` that doesn’t. Opinionated/low value. | | `-bugprone-easily-swappable-parameters` | Fires on any function with consecutive parameters of the same type (e.g., `(int width, int height)`). Extremely noisy for a codebase with many similarly-typed parameters in media/audio APIs. | | `-performance-enum-size` | Fairly noisy and low impact performance-wise. Possibly good to turn on in the future. | | `-modernize-return-braced-init-list` | Decided against it during review. | @@ -80,5 +80,4 @@ These options control tool-wide behavior or individual checks. | Setting | Reasoning | | --- | --- | -| `HeaderFilterRegex` | Only lint headers owned by this project (`include/livekit`, `src`, `bridge`, `examples`). Avoids flagging third-party/system headers. | | `modernize-use-nullptr.NullMacros: NULL` | Tells the nullptr modernizer to also replace `NULL` macros (common in C-interop code from the FFI bridge). | From 43fe313f76f6f032b208f69582fc0cb44b4a30c0 Mon Sep 17 00:00:00 2001 From: Alan George Date: Fri, 17 Jul 2026 15:27:27 -0600 Subject: [PATCH 24/24] Add style section --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 0c7e756..0494bd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,14 @@ repository has an `AGENTS.md` with conflicting rules they should take priority. - Keep third-party implementation details out of public headers and ABI boundaries. +## Style + +- Add the LiveKit copyright header with the correct year to new code files. +- Prefer the constructor initializer list rather than variable declaration + and assignment in the constructor body. +- For Doxygen/doc comments, prefer `///` comment style and use @brief, + @param, @return, @throw, @ref, @note, @warning as applicable. + ## Project-Owned clangd Configuration - Each consuming project must provide its own `.clangd`; compilation database