diff --git a/.github/actions/run-clang-format/action.yaml b/.github/actions/run-clang-format/action.yaml index 3883a1bd4eb50d..ddbe106f1029b5 100644 --- a/.github/actions/run-clang-format/action.yaml +++ b/.github/actions/run-clang-format/action.yaml @@ -47,6 +47,7 @@ runs: working-directory: ${{ inputs.workingDirectory }} env: CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} + FAIL_MODE: ${{ inputs.failCondition }} run: | : Run clang-format 🐉 if (( ${+RUNNER_DEBUG} )) setopt XTRACE @@ -59,5 +60,11 @@ runs: print ::group::Run clang-format-22 local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) - ./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check ${changes} + + if [[ "${FAIL_MODE:-never}" == 'never' ]] { + setopt NO_ERR_EXIT + setopt NO_ERR_RETURN + } + + ./build-aux/run-clang-format --check --github ${changes} print ::endgroup:: diff --git a/.github/actions/run-gersemi/action.yaml b/.github/actions/run-gersemi/action.yaml index 9a94d91a522cea..0962a0789a1f65 100644 --- a/.github/actions/run-gersemi/action.yaml +++ b/.github/actions/run-gersemi/action.yaml @@ -46,17 +46,22 @@ runs: working-directory: ${{ github.workspace }} env: CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} + FAIL_MODE: ${{ inputs.failCondition }} run: | : Run gersemi 🎛️ if (( ${+RUNNER_DEBUG} )) setopt XTRACE print ::group::Install gersemi brew update - brew trust obsproject/tools - brew install --quiet obsproject/tools/gersemi@0.25 + brew install --quiet gersemi print ::endgroup:: + if [[ "${FAIL_MODE:-never}" == 'never' ]] { + setopt NO_ERR_EXIT + setopt NO_ERR_RETURN + } + print ::group::Run gersemi local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) - ./build-aux/run-gersemi --fail-${{ inputs.failCondition }} --check ${changes} + ./build-aux/run-gersemi --check --github ${changes} print ::endgroup:: diff --git a/.github/actions/run-swift-format/action.yaml b/.github/actions/run-swift-format/action.yaml index 40bfe93fec43ca..e03ddaac6eafe6 100644 --- a/.github/actions/run-swift-format/action.yaml +++ b/.github/actions/run-swift-format/action.yaml @@ -46,6 +46,7 @@ runs: working-directory: ${{ github.workspace }} env: CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} + FAIL_MODE: ${{ inputs.failCondition }} run: | : Run swift-format 🔥 if (( ${+RUNNER_DEBUG} )) setopt XTRACE @@ -55,7 +56,12 @@ runs: brew install --quiet swift-format print ::endgroup:: + if [[ "${FAIL_MODE:-never}" == 'never' ]] { + setopt NO_ERR_EXIT + setopt NO_ERR_RETURN + } + print ::group::Run swift-format local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) - ./build-aux/run-swift-format --fail-${{ inputs.failCondition }} --check ${changes} + ./build-aux/run-swift-format --check --github ${changes} print ::endgroup:: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ae56113192815..4769299d41d1dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.28...3.30) -include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE) +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake") project(obs-studio VERSION ${OBS_VERSION_CANONICAL}) diff --git a/CMakePresets.json b/CMakePresets.json index f2f5a7e676699f..4d39d92a1fd96e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -88,6 +88,7 @@ "displayName": "macOS", "description": "Default macOS build (single architecture only)", "inherits": ["environmentVars"], + "warnings": {"dev": true, "deprecated": true}, "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -96,6 +97,7 @@ "generator": "Xcode", "binaryDir": "${sourceDir}/build_macos", "cacheVariables": { + "CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_ENABLE_CACHING": "YES", "CMAKE_OSX_DEPLOYMENT_TARGET": {"type": "STRING", "value": "13.0"}, "OBS_CODESIGN_IDENTITY": {"type": "STRING", "value": "$penv{CODESIGN_IDENT}"}, "OBS_CODESIGN_TEAM": {"type": "STRING", "value": "$penv{CODESIGN_TEAM}"}, @@ -113,11 +115,13 @@ "displayName": "macOS (CI)", "description": "CI macOS build (single architecture only)", "inherits": ["macos"], - "warnings": {"dev": true, "deprecated": true}, + "errors": {"dev": true, "deprecated": true}, "cacheVariables": { "CMAKE_COMPILE_WARNING_AS_ERROR": true, - "CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_ENABLE_CACHING": "YES", - "CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_CAS_PATH": "$penv{XCODE_CAS_PATH}" + "CMAKE_POLICY_DEFAULT_CMP0179": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0181": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0199": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0200": "NEW" } }, { @@ -145,10 +149,15 @@ { "name": "ubuntu-ci", "inherits": ["ubuntu"], + "errors": {"dev": true, "deprecated": true}, "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "CMAKE_COMPILE_WARNING_AS_ERROR": true, "CMAKE_COLOR_DIAGNOSTICS": true, + "CMAKE_POLICY_DEFAULT_CMP0179": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0181": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0199": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0200": "NEW", "ENABLE_CCACHE": true } }, @@ -157,6 +166,7 @@ "displayName": "Windows x64", "description": "Default Windows build (x64)", "inherits": ["environmentVars"], + "warnings": {"dev": true, "deprecated": true}, "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -176,9 +186,13 @@ "displayName": "Windows x64 (CI)", "description": "CI Windows build (x64)", "inherits": ["windows-x64"], - "warnings": {"dev": true, "deprecated": true}, + "errors": {"dev": true, "deprecated": true}, "cacheVariables": { - "CMAKE_COMPILE_WARNING_AS_ERROR": true + "CMAKE_COMPILE_WARNING_AS_ERROR": true, + "CMAKE_POLICY_DEFAULT_CMP0179": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0181": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0199": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0200": "NEW" } }, { @@ -186,6 +200,7 @@ "displayName": "Windows ARM64", "description": "Default Windows build (ARM64)", "inherits": ["environmentVars"], + "warnings": {"dev": true, "deprecated": true}, "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -208,9 +223,13 @@ "displayName": "Windows ARM64 (CI)", "description": "CI Windows build (ARM64)", "inherits": ["windows-arm64"], - "warnings": {"dev": true, "deprecated": true}, + "errors": {"dev": true, "deprecated": true}, "cacheVariables": { - "CMAKE_COMPILE_WARNING_AS_ERROR": true + "CMAKE_COMPILE_WARNING_AS_ERROR": true, + "CMAKE_POLICY_DEFAULT_CMP0179": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0181": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0199": "NEW", + "CMAKE_POLICY_DEFAULT_CMP0200": "NEW" } } ], diff --git a/build-aux/.run-format-launcher b/build-aux/.run-format-launcher new file mode 100755 index 00000000000000..fccf6e8e75bad8 --- /dev/null +++ b/build-aux/.run-format-launcher @@ -0,0 +1,38 @@ +#!/usr/bin/env sh + +# Combined POSIX Csh/Powershell entry point script +# Based on StackOverflow answer by Jeff Hykin +# https://stackoverflow.com/a/67292076 +# +# In Csh the first line will echo "--%" to /dev/null, followed by the colon built-in +# (using the semi-colon to create a command list). This built-in does nothing apart +# from variable expansions, and is given a multi-line string. +# +# In Powershell the line uses the '--%' token to stop parsing, which will also send the +# 'echo'. output to Powershell's null output, and will then encounter the declaration +# of a multiline comment, which extends until after the "exit" command. +# +# Thus Powershell will execute the code below "exit" in earnest, whereas Csh will ignore +# anything past that "exit". +# + +echo --% >/dev/null;: ' | out-null +<#' +host_os="$(uname)" +script_location="$(dirname "${0}")" +linter_name="$(basename "${0}")" +linter_name="${linter_name#*-}" + +if [ "${host_os}" = 'Darwin' ]; then + exec zsh "${script_location}/.run-format.zsh" --linter "${linter_name}" "${@}" +elif command -v zsh > /dev/null; then + exec zsh "${script_location}/.run-format.zsh" --linter "${linter_name}" "${@}" +else + exec bash "${script_location}/.run-format.bash" --linter "${linter_name}" "${@}" +fi +exit #> + +$ScriptLocation = $PSScriptRoot +$LinterName = ($MyInvocation.MyCommand.Name) -replace 'run-(.+).ps1','$1' + +. ${ScriptLocation}/.run-format.ps1 -Linter "${LinterName}" @Args diff --git a/build-aux/.run-format.bash b/build-aux/.run-format.bash new file mode 100755 index 00000000000000..80046e1e5093bf --- /dev/null +++ b/build-aux/.run-format.bash @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail + +## Enable for script debugging +# set -x + +shopt -s extglob +shopt -s globstar + +check_version() { + local version=() + local checked_version=() + + { IFS='.' read -r -a version; } <<< "${1}" + { IFS='.' read -r -a checked_version; } <<< "${2}" + + if (( version[0] >= checked_version[0] + && version[1] >= checked_version[1] + && version[2] >= checked_version[2] )); then + return 0 + else + return 1 + fi +} + +check_linter() { + local -i found=0 + local linter="${1}" + local min_version='' + local version_number='' + + case "${linter}" in + clang-format) + if command -v clang-format-22 > /dev/null; then + linter='clang-format-22' + found=1 + elif command -v clang-format > /dev/null; then + linter='clang-format' + found=1 + fi + + if (( found )); then + min_version='22.1.3' + + local -a clang_format_version + read -r -a clang_format_version <<< "$("${linter}" --version 2>/dev/null || true)" + local -i last_index="$(( ${#clang_format_version[@]} - 1 ))" + version_number="${clang_format_version[${last_index}]}" + fi + ;; + swift-format) + if command -v swift-format > /dev/null; then + linter='swift-format' + found=1 + fi + + if (( found )); then + min_version='602.0.0' + version_number="$(swift-format --version 2>/dev/null || true)" + fi + ;; + gersemi) + if command -v gersemi > /dev/null; then + linter='gersemi' + found=1 + fi + + if (( found )); then + min_version='0.27.0' + local -a gersemi_version + read -r -a gersemi_version <<< "$(gersemi --version 2>/dev/null || true)" + version_number="${gersemi_version[1]}" + fi + ;; + zizmor) + if command -v zizmor > /dev/null; then + linter='zizmor' + found=1 + fi + + if (( found )); then + min_version='1.25.0' + local -a zizmor_version + read -r -a zizmor_version <<< "$(zizmor --version 2>/dev/null || true)" + version_number="${zizmor_version[1]}" + fi + ;; + xmllint) + if command -v xmllint > /dev/null; then + linter='xmllint' + found=1 + fi + + if (( found )); then + min_version='20900.0.0' + local -a xmllint_version + read -r -a xmllint_version <<< "$(xmllint --version 2>&1 || true)" + version_number="${xmllint_version[4]}.0.0" + fi + ;; + *) + echo " ${_red}✖${_reset} Unsupported linter specified." + return 1 + ;; + esac + + if (( ! found )); then + echo " ${_red}✖${_reset} Unable to find '${linter}' on system." + return 1 + fi + + if ! check_version "${version_number}" "${min_version}"; then + echo " ${_red}✖${_reset} ${linter} ${version_number} found (Required: ${min_version})." + return 1 + fi +} + +generate_file_list() { + local linter="${1}" + + if (( ! ${#source_files[@]} )); then + case "${linter}" in + clang-format) + source_files=(@(libobs|libobs-*|frontend|plugins|deps|shared|test)/**/*.@(c|cpp|h|hpp|m|mm)) + read -r -a source_files <<< "${source_files[@]//*\/@(decklink\/*\/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)\/*/}" + ;; + swift-format) + source_files=(@(libobs|libobs-*|frontend|plugins)/**/*.swift) + ;; + gersemi) + source_files=(CMakeLists.txt @(libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/@(CMakeLists.txt|*.cmake)) + read -r -a source_files <<< "${source_files[@]//*\/@(jansson|decklink\/*\/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)\/*/}" + ;; + zizmor) + source_files=(.github/@(workflows|actions)/**/*.@(yaml|yml)) + ;; + xmllint) + source_files=(frontend/forms/**/*.ui) + ;; + *) ;; + esac + else + source_files=("${source_files[@]//${project_root}\/}") + fi +} + +invoke_formatter() { + local formatter="${1}" + shift + local -a source_files + read -r -a source_files <<< "${@}" + local -a format_arguments + + generate_file_list "${formatter}" + + case "${formatter}" in + clang-format) + format_arguments=(--style=file --fallback-style=none -i) + if (( verbose_output )); then + format_arguments+=('--verbose') + fi + ;; + swift-format) + format_arguments=(format --parallel --color-diagnostics -i) + ;; + gersemi) + format_arguments=(--no-cache -i) + ;; + *) + return 1 + ;; + esac + + "${formatter}" "${format_arguments[@]}" "${source_files[@]}" +} + +invoke_linter() { + local linter="${1}" + shift + local -a source_files + read -r -a source_files <<< "${@}" + + local regexp + local -a indices + local -a lint_arguments + + generate_file_list "${linter}" + + case "${linter}" in + clang-format) + regexp='^([^:]+):([0-9]+):[0-9]+:[[:space:]](.+):[[:space:]](.+)\[-W(.+)\]$' + indices=(1 2 3 5 4) + lint_arguments=(--style=file --fallback-style=none -Werror --dry-run) + if (( verbose_output )); then + lint_arguments+=(--verbose) + fi + ;; + swift-format) + regexp='^([^:]+):([0-9]+):[0-9]+:[[:space:]](.+):[[:space:]]\[(.+)\][[:space:]](.+)$' + indices=(1 2 3 4 5) + lint_arguments=(lint) + ;; + gersemi) + regexp="^${project_root}/([^[:space:]]+)[[:space:]](.+)" + indices=(1 'Entire File' 'error' 'gersemi' 2) + lint_arguments=(--check --no-cache --warnings-as-errors) + ;; + zizmor) + regexp='^::(.+)[[:space:]]file=(.+),line=([0-9]+),title=(.+)::.+:[0-9]+:[[:space:]](.+)$' + indices=(2 3 1 4 5) + lint_arguments=(--offline --persona=auditor --format=github --no-progress --quiet) + ;; + xmllint) + regexp='^([^:]+):([0-9]+):[[:space:]]+.+:[[:space:]](.+):[[:space:]](.+)$' + indices=(1 2 error 3 4) + lint_arguments=(--schema "${project_root}/frontend/forms/XML-Schema-Qt5.15.xsd" --noout) + ;; + *) + return 1 + ;; + esac + + local -i num_failures=0 + + if (( ${#source_files[@]} )); then + local line + local file_path + local file_name + local line_number + local error_level + local error_title + local error_message + local -a BASH_REMATCH + while read -r line; do + local ordered_output='' + if [[ "${linter}" == 'zizmor' ]] && (( github_style )); then + echo "${line}" + num_failures+=1 + continue + fi + + if [[ "${line}" =~ ${regexp} ]]; then + for index in "${indices[@]}"; do + if [[ "${index}" = [[:digit:]] ]]; then + ordered_output+="${BASH_REMATCH[${index}]}|" + else + ordered_output+="${index}|" + fi + done + + { IFS="|" read -r file_path line_number error_level error_title error_message; } <<< "${ordered_output}" + + if (( github_style )); then + file_name="$(basename "${file_path}")" + echo "::${error_level} file=${file_path},line=${line_number},title=${error_title}::${file_name}:${line_number}: ${error_message}" + else + echo " ${_red}✖${_reset} ${file_path}:${line_number} - ${error_title}: ${error_message}" + fi + + num_failures+=1 + else + echo "${line}" + fi + done < <("${linter}" "${lint_arguments[@]}" "${source_files[@]}" 2>&1 || true) + fi + + if (( num_failures )); then + return 1 + fi +} + +main() { + local _red='' + local _reset='' + + if [[ -z "${CI:-}" ]]; then + _red="$(tput setaf 1)" + _reset="$(tput sgr0)" + fi + + if (( BASH_VERSINFO[0] < 4 )); then + echo " ${_red}✖${_reset} ${0} requires Bash 4.0 or later (detected version: ${BASH_VERSION})." + exit 1 + fi + + if [[ -z "${SCRIPT_HOME:-}" ]]; then + local script_realpath + local script_dirname + + script_realpath="$(realpath "${0}")" + script_dirname="$(dirname "${script_realpath}")" + + typeset -g SCRIPT_HOME="${script_dirname}" + fi + + local project_root + project_root="$(dirname "${SCRIPT_HOME}")" + + local -i verbose_output=0 + local -i lint_only=0 + local -i github_style=0 + + local -a args + while (( ${#} )); do + case "${1}" in + -c|--check) lint_only=1; shift ;; + -v|--verbose) verbose_output=1; shift ;; + -gh|--github) github_style=1; shift ;; + -l|--linter) + if [[ -z "${LINTER_NAME:-}" && -n "${2}" ]]; then + typeset -g LINTER_NAME="${2}" + shift 2 + fi + ;; + *) + args+=("${1}") + shift + ;; + esac + done + + check_linter "${LINTER_NAME}" + + if (( lint_only )); then + invoke_linter "${LINTER_NAME}" "${args[@]}" + else + invoke_formatter "${LINTER_NAME}" "${args[@]}" + fi +} + +main "${@}" diff --git a/build-aux/.run-format.ps1 b/build-aux/.run-format.ps1 new file mode 100644 index 00000000000000..4272e93522bd68 --- /dev/null +++ b/build-aux/.run-format.ps1 @@ -0,0 +1,450 @@ +#Requires -Version 7.3 + +[CmdletBinding(PositionalBinding=$false)] +param( + [ValidateSet('clang-format', 'swift-format', 'gersemi', 'zizmor', 'xmllint')] + [string] $Linter, + [switch] $Check, + [switch] $GitHubStyle, + [Parameter(ValueFromRemainingArguments)] + [string[]] $SourceFiles +) + +begin { + $_EAP = $ErrorActionPreference + $_DP = $DebugPreference + $_VP = $VerbosePreference + $_IP = $InformationPreference + + $ErrorActionPreference = 'Stop' + + if ( $DebugPreference -eq 'Continue' ) { + $VerbosePreference = 'Continue' + $InformationPreference = 'Continue' + } + + $Sep = [System.IO.Path]::DirectorySeparatorChar + $Sep = "${Sep}${Sep}" + + function Invoke-External { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true, Position=0)] + [string] $Command, + [Parameter(ValueFromRemainingArguments, Position=1)] + [string[]] $Arguments + ) + + begin { + $_EAP = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + } + + process { + Write-Debug "Invoke-External: ${Command} ${Arguments} 2>&1" + + & $Command @Arguments 2>&1 + $Result = $LASTEXITCODE + } + + end { + $ErrorActionPreference = $_EAP + + if ( $Result -ne 0 ) { + throw "${Command} ${Arguments} exited with non-zero code ${Result}." + } + } + } + + function Test-CommandExists { + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [String] $Command + ) + + process { + try { + Get-Command $Command -ErrorAction 'Stop' + $true + } catch { + $false + } + } + } + + function Check-Linter { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('clang-format', 'swift-format', 'gersemi', 'zizmor', 'xmllint')] + [string] $Linter + ) + + begin { + $Found = $false + + $LinterCommand = $null + $MinimumVersion = $null + $VersionNumber = $null + } + + process { + switch ($Linter) { + clang-format { + if ((Test-CommandExists 'clang-format-22')) { + $LinterCommand = Get-Command 'clang-format-22' + } elseif ((Test-CommandExists 'clang-format')) { + $LinterCommand = Get-Command 'clang-format' + } else { + break + } + + $MinimumVersion = New-Object -TypeName System.Version -ArgumentList '22.1.3' + + $ClangFormatVersion = (($( Invoke-External $LinterCommand --version ) -split ' ')[2]) + $VersionNumber = New-Object -TypeName System.Version -ArgumentList $ClangFormatVersion + + $Found = $true + break + } + gersemi { + if ((Test-CommandExists 'gersemi')) { + $LinterCommand = Get-Command 'gersemi' + } else { + break + } + + $MinimumVersion = New-Object -TypeName System.Version -ArgumentList '0.27.0' + + $GersemiVersion = (($( Invoke-External $LinterCommand --version ) -split ' ')[1]) + $VersionNumber = New-Object -TypeName System.Version -ArgumentList $GersemiVersion + + $Found = $true + break + } + zizmor { + if ((Test-CommandExists 'zizmor')) { + $LinterCommand = Get-Command 'zizmor' + } else { + break + } + + $MinimumVersion = New-Object -TypeName System.Version -ArgumentList '1.25.0' + + $ZizmorVersion = (($( Invoke-External $LinterCommand --version ) -split ' ')[1]) + $VersionNumber = New-Object -TypeName System.Version -ArgumentList $ZizmorVersion + + $Found = $true + break + } + default { + throw "Unsupported linter '${Linter}' specified." + } + } + } + + end { + if ($Found -eq $false) { + throw "Unable to find '${Linter}' on system." + } + + if (!($VersionNumber -ge $MinimumVersion)) { + throw "${Linter} ${VersionNumber} found (Required: ${MinimumVersion})." + } + + $LinterCommand + } + } + + function Generate-File-List { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('clang-format', 'swift-format', 'gersemi', 'zizmor', 'xmllint')] + [string] $Linter + ) + + begin { + $Files = $null + $ProjectRootPattern = "^$([regex]::Escape($ProjectRoot))${Sep}" + } + + process { + + switch ($Linter) { + clang-format { + $Directories = Get-ChildItem -Path $ProjectRoot -Attribute Directory | Where-Object { + $_.Name -match '^(libobs*|frontend|plugins|deps|shared|test)' + } + + $Pattern = ".*${Sep}(decklink${Sep}.+${Sep}decklink-sdk|obs-websocket|obs-browser|libdshowcapture)" + $Files = $Directories | ForEach-Object { + Get-ChildItem -Path $_ -Recurse -File -Include '*.c','*.h','*.m','*.hpp','*.cpp','*.mm' + } | Where-Object { + ! ($_.Directory.FullName -match $Pattern) + } | ForEach-Object { + $Sep = [System.IO.Path]::DirectorySeparatorChar + ($_.FullName) -replace $ProjectRootPattern,".${Sep}" + } + break + } + gersemi { + $Directories = Get-ChildItem -Path $ProjectRoot -Attribute Directory | Where-Object { + $_.Name -match '^(libobs*|frontend|plugins|deps|shared|cmake|test)' + } + + $Pattern = ".*${Sep}(jansson|decklink${Sep}.+${Sep}decklink-sdk|libdshowcapture|obs-websocket|obs-browser)" + $Files = $Directories | ForEach-Object { + Get-ChildItem -Path $_ -Recurse -File -Include '*.cmake','CmakeLists.txt' + } | Where-Object { + ! ($_.Directory.FullName -match $Pattern) + } | ForEach-Object { + $Sep = [System.IO.Path]::DirectorySeparatorChar + ($_.FullName) -replace $ProjectRootPattern,".${Sep}" + } + $Files += ".${Sep}CMakeLists.txt" + break + } + zizmor { + $Directories = Get-ChildItem -Path $ProjectRoot -Attribute Directory | Where-Object { + $_.Name -match '^.github/(workflows|actions)' + } + $Files = $Directories | ForEach-Object { + Get-ChildItem -Path $_ -Recurse -File -Include '*.yaml','*.yml' + } | ForEach-Object { + $Sep = [System.IO.Path]::DirectorySeparatorChar + ($_.FullName) -replace $ProjectRootPattern,".${Sep}" + } + break + } + default { + break + } + } + } + + end { + $Files + } + } + + function Invoke-Formatter { + [CmdletBinding(PositionalBinding=$false)] + param( + [Parameter(Mandatory)] + [ValidateSet('clang-format', 'swift-format', 'gersemi', 'zizmor', 'xmllint')] + [string] $Formatter, + [Parameter(Mandatory)] + [object] $FormatterCommand, + [Parameter(ValueFromRemainingArguments)] + [string[]] $SourceFiles + ) + + begin { + $FormatterArguments = $null + $TempFile = New-TemporaryFile + } + + process { + switch ($Formatter) { + clang-format { + $FormatterArguments = @( + '--style=file' + '--fallback-style=none' + $( if($VerbosePreference -eq 'Continue') {'--verbose'} ) + '-i' + ) + break + } + gersemi { + $FormatterArguments = @( + '--no-cache' + '-i' + ) + break + } + default { + throw "Unsupported formatter '${Formatter}' specified" + } + } + + # Special handling for clang-format: Due to the amount of eligible source files in the project, + # a command line listing all files to format will exceed Windows's limit of 8191 characters. + # clang and clang-format support a special mode to provide the list of files in a separate file, + # which is used here. + if ($Formatter -eq 'clang-format') { + $SourceFiles | Out-File -FilePath $TempFile + $FilesToFormat = @( "@$( $TempFile.FullName )" ) + } else { + $FilesToFormat = $SourceFiles + } + + try { + Invoke-External $FormatterCommand.Source @FormatterArguments @FilesToFormat + } catch {} + } + + end { + Remove-Item $TempFile + } + } + + function Invoke-Linter { + [CmdletBinding(PositionalBinding=$false)] + param( + [Parameter(Mandatory)] + [ValidateSet('clang-format', 'swift-format', 'gersemi', 'zizmor', 'xmllint')] + [string] $Linter, + [Parameter(Mandatory)] + [object] $LinterCommand, + [Parameter(ValueFromRemainingArguments)] + [string[]] $SourceFiles + ) + + begin { + $LinterArguments = $null + $RegexpPattern = $null + $Indices = $null + + $NumFailures = 0 + + $TempFile = New-TemporaryFile + } + + process { + switch ( $Linter ) { + clang-format { + $RegexpPattern = '^([^:]+):([0-9]+):[0-9]+:\s(.+):\s(.+)\[-W(.+)\]$' + $Indices = @(1,2,3,5,4) + $LinterArguments = @( + '--style=file' + '--fallback-style=none' + '-Werror' + '--dry-run' + $( if($VerbosePreference -eq 'Continue') {'--verbose'} ) + ) + break + } + gersemi { + $RegexpPattern = "^$([regex]::Escape($ProjectRoot))${Sep}([^\s]+)\s(.+)" + $Indices = @(1,'Entire File','error','gersemi',2) + $LinterArguments = @( + '--check' + '--no-cache' + '--warnings-as-errors' + ) + break + } + zizmor { + $RegexpPattern = '^::(.+)\sfile=(.+),line=([0-9]+),title=(.+)::.+:[0-9]+:\s(.+)$' + $Indices = @(2,3,1,4,5) + $LinterArguments = @( + '--offline' + '--persona=auditor' + '--format=github' + '--no-progress' + '--quiet' + ) + break + } + default { + throw "Unsupported linter '${Linter}' specified" + } + } + + # Special handling for clang-format: Due to the amount of eligible source files in the project, + # a command line listing all files to format will exceed Windows's limit of 8191 characters. + # clang and clang-format support a special mode to provide the list of files in a separate file, + # which is used here. + if ($Linter -eq 'clang-format') { + $SourceFiles | Out-File -FilePath $TempFile + $FilesToFormat = @( "@$($TempFile.FullName)" ) + } else { + $FilesToFormat = $SourceFiles + } + + try { + Invoke-External $LinterCommand.Source @LinterArguments @FilesToFormat | ForEach-Object { + if (($Linter -eq 'zizmor') -and ($script:GitHubStyle)) { + Write-Host $_ + $NumFailures += 1 + continue + } + + $Matched = $_ -match $RegexpPattern + + if ($Matched -eq $true) { + $FilePath, $LineNumber, $ErrorLevel, $ErrorTitle, $ErrorMessage = $Indices | ForEach-Object { + $Matches[$_] ?? $_ + } + + $FilePath = ($FilePath -replace '\\','/') -replace '\./','/' + if ($script:GitHubStyle) { + $FileName = ($FilePath | Get-Item).Name + Write-Host "::${ErrorLevel} file=${FilePath},line=${LineNumber},title=${ErrorTitle}::${FileName}:${LineNumber}: ${ErrorMessage}" + } else { + Write-Host -NoNewLine -ForegroundColor Red " ✖ " + Write-Host "${FilePath}:${LineNumber} - ${ErrorTitle}: ${ErrorMessage}" + } + $NumFailures += 1 + } else { + Write-Host $_ + } + } + } catch {} + } + + end { + Remove-Item $TempFile + + $NumFailures + } + } +} + +process { + $ScriptHome = $PSScriptRoot + $ProjectRoot = ($PSScriptRoot | Get-Item).Parent + + $LinterCommand = $null + + if (($null -eq $Linter) -and ($null -ne $env:LINTER_COMMAND)) { + $Linter = $env:LINTER_COMMAND + } + + $LinterCommand = Check-Linter -Linter $Linter + + if ($Linter -eq $null) { + throw 'No linter detected or provided via ''LINTER_COMMAND'' environment variable.' + } + + if ($SourceFiles -eq $null ) { + $SourceFiles = Generate-File-List -Linter $Linter + } else { + $SourceFiles = Get-ChildItem -Path ${SourceFiles} | ForEach-Object { + ($_.FullName) -replace "^$([regex]::Escape($ProjectRoot))${Sep}",'' + } + } + + $NumFailures = 0 + + if ($script:Check) { + $NumFailures = Invoke-Linter -LinterCommand $LinterCommand -Linter $Linter @SourceFiles + } else { + Invoke-Formatter -FormatterCommand $LinterCommand -Formatter $Linter @SourceFiles + } +} + +end { + $ErrorActionPreference = $_EAP + $DebugPreference = $_DP + $VerbosePreference = $_VP + $InformationPreference = $_IP + + if ($NumFailures -gt 0) { + exit 1 + } else { + exit 0 + } +} diff --git a/build-aux/.run-format.zsh b/build-aux/.run-format.zsh index 9146c087f37285..72e8c756fc06ec 100755 --- a/build-aux/.run-format.zsh +++ b/build-aux/.run-format.zsh @@ -1,295 +1,306 @@ #!/usr/bin/env zsh builtin emulate -L zsh -setopt EXTENDED_GLOB -setopt PUSHD_SILENT setopt ERR_EXIT setopt ERR_RETURN -setopt NO_UNSET -setopt PIPE_FAIL +setopt EXTENDED_GLOB +setopt FUNCTION_ARGZERO setopt NO_AUTO_PUSHD setopt NO_PUSHD_IGNORE_DUPS -setopt FUNCTION_ARGZERO +setopt NO_UNSET +setopt PIPE_FAIL +setopt PUSHD_SILENT +setopt WARN_CREATE_GLOBAL +setopt WARN_NESTED_VAR ## Enable for script debugging -# setopt WARN_CREATE_GLOBAL -# setopt WARN_NESTED_VAR # setopt XTRACE -autoload -Uz is-at-least && if ! is-at-least 5.2; then - print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade zsh to fix this issue." - exit 1 -fi - -invoke_formatter() { - if (( # < 1 )) { - log_error "Usage invoke_formatter [formatter_name]" - exit 2 - } - - local formatter="${1}" - shift - local -a source_files=(${@}) +check_linter() { + local -i found=0 + local linter=${1} + local min_version + local version_number - case ${formatter} { - clang) + case ${linter} { + clang-format) if (( ${+commands[clang-format-22]} )) { - local formatter=clang-format-22 + linter='clang-format-22' + found=1 } elif (( ${+commands[clang-format]} )) { - local formatter=clang-format - } else { - log_error "No viable clang-format version found (required 22.1.3)" - exit 2 + linter='clang-format' + found=1 } - local -a formatter_version=($(${formatter} --version)) - - if ! is-at-least 22.1.3 ${formatter_version[-1]}; then - log_error "clang-format is not version 22.1.3 or above (found ${formatter_version[-1]}." - exit 2 - fi - - if ! is-at-least ${formatter_version[-1]} 22.1.3; then - log_warning "clang-format is more recent than version 22.1.3 (found ${formatter_version[-1]})." - fi - - if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins|deps|shared|test)/**/*.(c|cpp|h|hpp|m|mm)(.N)) + if (( found )) { + min_version='22.1.3' - source_files=(${source_files:#*/(obs-websocket/deps|decklink/*/decklink-sdk|mac-syphon/syphon-framework|libdshowcapture)/*}) - - local -a format_args=(-style=file -fallback-style=none) - if (( _loglevel > 2 )) format_args+=(--verbose) - - check_files() { - local -i num_failures=0 - local -a source_files=($@) - local file - local -a format_args=(-style=file -fallback-style=none) - if (( _loglevel > 2 )) format_args+=(--verbose) - - local -a command=(${formatter} ${format_args}) - - for file (${source_files}) { - if ! ${command} "${file}" | diff -q "${file}" - &> /dev/null; then - log_error "${file} requires formatting changes." - if (( fail_on_error == 2 )) return 2; - num_failures=$(( num_failures + 1 )) - fi - } - if (( num_failures && fail_on_error == 1 )) return 2 + local -a clang_format_version + read -r -A clang_format_version <<< "$(${linter} --version 2>/dev/null || true)" + version_number=${clang_format_version[-1]} + } + ;; + swift-format) + if (( ${+commands[swift-format]} )) { + linter='swift-format' + found=1 } - format_files() { - local -a source_files=($@) - - if (( ${#source_files} )) { - local -a format_args=(-style=file -fallback-style=none -i) - if (( _loglevel > 2 )) format_args+=(--verbose) - - "${formatter}" ${format_args} ${source_files} - } + if (( found )) { + min_version='602.0.0' + version_number="$(swift-format --version 2>/dev/null || true)" } ;; gersemi) - - if (( ${+commands[gersemi-0.25]} )) { - local formatter=gersemi-0.25 - } elif (( ${+commands[gersemi]} )) { - local formatter=gersemi - } else { - log_error "No viable gersemi version found (required 0.25.0)" - exit 2 + if (( ${+commands[gersemi]} )) { + linter='gersemi' + found=1 } - local gersemi_version=($(${formatter} --version)) - - if ! is-at-least 0.25.0 ${gersemi_version[2]}; then - log_error "gersemi is not version 0.25.0 or above (found ${gersemi_version[2]}." - exit 2 - fi - - if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) - - source_files=(${source_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) - source_files=(${source_files:#(cmake/Modules/*|*/legacy.cmake)}) - - check_files() { - local -i num_failures=0 - local -a source_files=($@) - local file - local -a command=(${formatter} -c --no-cache ${source_files}) - local -i in_error=0 - - if (( ${#source_files} )) { - while read -r line; do - local -a line_tokens=(${(z)line}) - if (( #line_tokens )) { - file=${line_tokens[1]} - - if [[ -r ${file} ]] { - in_error=0 - file=${file//*${project_root}\//} - - log_error "${file} requires formatting changes." - } else { - if (( in_error )) { - log_output "${line}" - } else { - log_error "${line}" - } - in_error=1 - } - } else { - log_output "${line}" - } - - if (( fail_on_error == 2 )) return 2 - num_failures=$(( num_failures + 1 )) - done < <(${command} 2>&1) - - if (( num_failures && fail_on_error == 1 )) return 2 - } + if (( found )) { + min_version='0.27.0' + local -a gersemi_version + read -r -A gersemi_version <<< "$(gersemi --version 2>/dev/null || true)" + version_number=${gersemi_version[2]} + } + ;; + zizmor) + if (( ${+commands[zizmor]} )) { + linter='zizmor' + found=1 } - format_files() { - local -a source_files=($@) - - if (( ${#source_files} )) { - "${formatter}" -i ${source_files} - } + if (( found )) { + min_version='1.25.0' + local -a zizmor_version + read -r -A zizmor_version <<< "$(zizmor --version 2>/dev/null || true)" + version_number=${zizmor_version[2]} } ;; - swift) - local formatter=swift-format - if (( ${+commands[swift-format]} )) { - local swift_format_version=$(swift-format --version) + xmllint) + if (( ${+commands[xmllint]} )) { + linter='xmllint' + found=1 + } - if ! is-at-least 508.0.0 ${swift_format_version}; then - log_error "swift-format is not version 508.0.0 or above (found ${swift_format_version})." - exit 2 - fi - } else { - log_error "No viable swift-format version found (required 508.0.0)" - exit 2 + if (( found )) { + min_version='20900.0.0' + local -a xmllint_version + read -r -A xmllint_version <<< "$(xmllint --version 2>&1 || true)" + version_number="${xmllint_version[5]}.0.0" } + ;; + *) + print -u2 -PR "%F{1} ✖ %f Unsupported linter specified." + return 1 + ;; + } - if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N)) + if (( ! found )) { + print -u2 -PR "%F{1} ✖ %f Unable to find %B'${linter}'%b on system." + return 1 + } - check_files() { - local -i num_failures=0 - local -a source_files=($@) - local file - local -a format_args=() + if ! is-at-least ${min_version} ${version_number}; then + print -u2 -PR "%F{1} ✖ %f ${linter} ${version_number} found (Required: %B${min_version}%b)." + return 1 + fi +} - local -a command=(${formatter} ${format_args}) +generate_file_list() { + local linter=${1} + local -a found_files - for file (${source_files}) { - if ! "${command}" "${file}" | diff -q "${file}" - &> /dev/null; then - log_error "${file} requires formatting changes." - if (( fail_on_error == 2 )) return 2; - num_failures=$(( num_failures + 1 )) - fi - } - if (( num_failures && fail_on_error == 1 )) return 2 - } + if (( ! #source_files )) { + case ${linter} { + clang-format) + found_files=((libobs|libobs-*|frontend|plugins|deps|shared|test)/**/*.(c|cpp|h|hpp|m|mm)(.N)) + found_files=(${found_files:#*/(decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) + ;; + swift-format) + found_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N)) + ;; + gersemi) + found_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) + found_files=(${found_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) + ;; + zizmor) + found_files=(.github/(workflows|actions)/**/*.(yaml|yml)) + ;; + xmllint) + found_files=(frontend/forms/**/*ui) + ;; + *) return ;; + } - format_files() { - local -a source_files=($@) + typeset -ga source_files=(${found_files}) + } else { + typeset -ga source_files=(${source_files//${project_root}\/}) + } +} - if (( ${#source_files} )) { - local -a format_args=(-i) +invoke_formatter() { + local formatter=${1} + shift + local -a source_files + read -r -A source_files <<< "${@}" + local -a format_arguments - "${formatter}" ${format_args} ${source_files} - } + generate_file_list ${formatter} + + case ${formatter} { + clang-format) + format_arguments=(--style=file --fallback-style=none -i) + if (( verbose_output )) { + format_arguments+=(--verbose) } ;; - *) log_error "Invalid formatter specified: ${1}. Valid options are clang-format, gersemi, and swift-format."; exit 2 ;; + swift-format) + format_arguments=(format --parallel --color-diagnostics -i) + ;; + gersemi) + format_arguments=(--no-cache -i) + ;; + *) + return 1 } - local file - local -i num_failures=0 - if (( check_only )) { - if (( ${+functions[check_files]} )) { - check_files ${source_files} - } else { - log_error "No format check function defined for formatter '${formatter}'" - exit 2 - } - } else { - if (( ${+functions[format_files]} )) { - format_files ${source_files} - } else { - log_error "No format function defined for formatter '${formatter}'" - exit 2 - } - } + ${formatter} ${format_arguments} ${source_files} } -run_format() { - if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} - if (( ! ${+FORMATTER_NAME} )) typeset -g FORMATTER_NAME=${${(s:-:)ZSH_ARGZERO:t:r}[2]} - local project_root=${SCRIPT_HOME:A:h} +invoke_linter() { + local linter=${1} + shift + local -a source_files + read -r -A source_files <<< "${@}" + + local regexp + local glob_expression + local -a indices + local -a lint_arguments + + generate_file_list ${linter} + + case ${linter} { + clang-format) + regexp='^([^:]+):([0-9]+):[0-9]+:[[:space:]](.+):[[:space:]](.+)\[-W(.+)\]$' + indices=(1 2 3 5 4) + lint_arguments=(--style=file --fallback-style=none -Werror --dry-run) + if (( verbose_output )) { + lint_arguments+=(--verbose) + } + ;; + swift-format) + regexp='^([^:]+):([0-9]+):[0-9]+:[[:space:]](.+):[[:space:]]\[(.+)\][[:space:]](.+)$' + indices=(1 2 3 4 5) + lint_arguments=(lint) + ;; + gersemi) + regexp="^${project_root}/([^[:space:]]+)[[:space:]](.+)" + indices=(1 'Entire File' 'error' 'gersemi' 2) + lint_arguments=(--check --no-cache --warnings-as-errors) + ;; + zizmor) + regexp='^::(.+)[[:space:]]file=(.+),line=([0-9]+),title=(.+)::.+:[0-9]+:[[:space:]](.+)$' + indices=(2 3 1 4 5) + lint_arguments=(--offline --persona=auditor --format=github --no-progress --quiet) + ;; + xmllint) + regexp='^([^:]+):([0-9]+):[[:space:]]+.+:[[:space:]](.+):[[:space:]](.+)$' + indices=(1 2 error 3 4) + lint_arguments=(--schema ${project_root}/frontend/forms/XML-Schema-Qt5.15.xsd --noout) + ;; + *) + return 1 + ;; + } - typeset -g host_os=${${(L)$(uname -s)}//darwin/macos} - local -i fail_on_error=0 - local -i check_only=0 - local -i verbosity=1 - local -r _version='1.0.0' + local -i num_failures=0 - fpath=("${SCRIPT_HOME}/.functions" ${fpath}) - autoload -Uz set_loglevel log_info log_error log_output log_status log_warning + if (( #source_files )) { + local file_path + local line_number + local error_level + local error_title + local error_message + + local line + local ordered_output + local -a match + local mbegin + local mend + local MATCH + local MBEGIN + local MEND + + while read -r line; do + if [[ ${linter} == 'zizmor' ]] && (( github_style )) { + print -- ${line} + num_failures+=1 + continue + } - local -r _usage=" -Usage: %B${functrace[1]%:*}%b