diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7707fc5 --- /dev/null +++ b/.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/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..925a8bd --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,43 @@ +Checks: > + -*, + clang-analyzer-*, + 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/.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 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0494bd8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,127 @@ +# AGENTS.md — Shared C++ Engineering Baseline + +## Scope + +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 + +- 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. + - `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 + 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. +- 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, + especially for media, sensor, and message data. +- Declare data at the smallest useful scope and initialize it before use. + +## 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 + 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, such as `_us` + for microseconds. 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. + +## 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 + 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. +- 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 b1170fd..4e28fc4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,138 @@ # cpp-tools -Standardized set of tools for use in LiveKit C++ projects. + +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/): 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 + +This repository is intended to be consumed as a git submodule. + +## Quick start + +Add this repository as a submodule from the consuming repository root: + +```bash +git submodule add https://github.com/livekit/cpp-tools.git cpp-tools +``` + +Install the shared configuration symlinks: + +```bash +./cpp-tools/install.sh # Installs .clang-format and .clang-tidy symlinks to repo root +``` + +Optionally install a precommit hook that automatically runs `clang-format` before commits: + +```bash +./cpp-tools/install.sh precommit-hook # Installs precommit hook +``` + +Run the tools from the repository root: + +```bash +# Check formatting or rewrite files in place. +./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 --file-regex '.*\.(c|cpp|cc|cxx)$' +./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 +## 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. +``` + +## clang-format + +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 path/to/sources \ + --path path/to/headers +``` + +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 + +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: + +```bash +./cpp-tools/clang-tidy.sh \ + --build-dir build-release \ + --file-regex '.*\.(c|cpp|cc|cxx)$' +``` + +Additional `run-clang-tidy` arguments can be passed after `--`. + +## 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. + +## 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 +`GITHUB_ACTIONS=true`. Consumer repositories own checkout, tool installation, +and project-specific build preparation, then call their project wrappers: + +```yaml +- 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 +``` + +`FORMAT_BLOB_SHA` and `TIDY_BLOB_SHA` make source links target the pull +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/clang-format.sh b/clang-format.sh new file mode 100755 index 0000000..02d7d7b --- /dev/null +++ b/clang-format.sh @@ -0,0 +1,371 @@ +#!/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. + Required unless FILE... or CLANG_FORMAT_PATHS is supplied. + --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. + CLANG_FORMAT_FIX_COMMAND + Local fix command shown in summaries. Defaults to a command matching + the paths or files supplied to this invocation. +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 + +files=() +if (( ${#explicit_files[@]} > 0 )); then + files=("${explicit_files[@]}") +else + if (( ${#paths[@]} == 0 )); then + 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}") + done < <(git ls-files -z -- \ + "${paths[@]}" \ + | 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 + +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}" + +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 \`${fix_command}\` 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 '${fix_command}' 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/clang-tidy.sh b/clang-tidy.sh new file mode 100755 index 0000000..849a7c2 --- /dev/null +++ b/clang-tidy.sh @@ -0,0 +1,408 @@ +#!/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. 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. + --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 + Environment equivalents for the matching options above. +EOF +} + +repo_root="${REPO_ROOT:-}" +build_dir="${CLANG_TIDY_BUILD_DIR:-build-release}" +file_regex="${CLANG_TIDY_FILE_REGEX:-}" +header_filter="${CLANG_TIDY_HEADER_FILTER:-}" +exclude_header_filter="${CLANG_TIDY_EXCLUDE_HEADER_FILTER:-}" +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 + ;; + --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 (( ${#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 +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 ! 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/docs/clang-tidy.md b/docs/clang-tidy.md new file mode 100644 index 0000000..2643faa --- /dev/null +++ b/docs/clang-tidy.md @@ -0,0 +1,83 @@ +# 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 | +| --- | --- | +| `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. | +| `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` | 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. | +| `-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 | +| --- | --- | +| `modernize-use-nullptr.NullMacros: NULL` | Tells the nullptr modernizer to also replace `NULL` macros (common in C-interop code from the FFI bridge). | diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4474cbc --- /dev/null +++ b/install.sh @@ -0,0 +1,210 @@ +#!/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 [COMPONENT...] [OPTIONS] + +Install shared clang configuration symlinks and, when requested, the +pre-commit hook. With no components, installs both configuration symlinks. + +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 + 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 +} + +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 + 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 + ;; + *) + 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 +fi + +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=() +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 + 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 + +if ((install_precommit == 1)); then + install_precommit_hook "${repo_root}" +fi