1+ #! /usr/bin/env bash
2+ set -e
3+
4+ # This script will get the diff between the current branch and the main branch.
5+ # It will then find all files that depend on these diff files, by
6+ # using clang-scan-deps to generate the dependency graph.
7+ # The clang-query is then run on these diffed files (and their dependants).
8+ # One weakness of this is that this diff only compares the current branch and main branch,
9+ # and so is mainly helpful for PRs. But doesn't help when committing directly to main, or
10+ # after merging the PR. This isn't insurmountable, but I felt it was getting too messy.
11+
12+
13+ # to get this script to work in Github CI:
14+ # Update cpp-ci-serial-programs-base.yml:
15+ # - Set fetch-depth to 0 to fetch all commits for the diff. with blob:none so you're not downloading all that data
16+ # with:
17+ # path: 'Arduino-Source'
18+ # submodules: 'recursive'
19+ # fetch-depth: 0
20+ # filter: blob:none
21+ #
22+ # - when installing clang-tools, specify a version. e.g. clang-tools-18
23+ # sudo apt install clang-tools-18 libopencv-dev
24+ #
25+ # - under run clang query: set working directory. run this script
26+ # name: Run clang query
27+ # if: inputs.run-clang-query
28+ # working-directory: ./Arduino-Source
29+ # run : bash ./.github/scripts/clang-query.sh
30+ # - this script should be placed within the folder .github/scripts
31+ #
32+ # other files to consider updating
33+ # .gitattributes: *.sh text eol=lf
34+
35+
36+ # to get this script to work locally in Windows:
37+ # Open Git Bash. cd to root of the repo (Arduino-Source). Run the following command:
38+ # sh .github/scripts/clang-query.sh
39+
40+
41+ SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
42+ REPO_ROOT=" $( cd " $SCRIPT_DIR /../.." && pwd) "
43+ TMP_DIR=" $REPO_ROOT /.ci_tmp"
44+ mkdir -p " $TMP_DIR "
45+
46+ # Define the cleanup function
47+ cleanup () {
48+ echo " Cleaning up temporary files..."
49+ rm -rf " $TMP_DIR "
50+ }
51+
52+ # Register the trap: run cleanup on EXIT, plus common signals like INT (Ctrl+C) or TERM
53+ # trap cleanup EXIT INT TERM
54+
55+
56+ cd " $REPO_ROOT "
57+
58+ # find path to compile_commands.json
59+ if [ -f " $REPO_ROOT /SerialPrograms/bin/compile_commands.json" ]; then
60+ DB_PATH=" $REPO_ROOT /SerialPrograms/bin/compile_commands.json"
61+ elif [ -f " $REPO_ROOT /build/RelWithDebInfo/compile_commands.json" ]; then
62+ DB_PATH=" $REPO_ROOT /build/RelWithDebInfo/compile_commands.json"
63+ else
64+ echo " Error: compile_commands.json not found!"
65+ exit 1
66+ fi
67+
68+
69+ echo " Generating clang-scan-deps experimental-full > deps.json."
70+
71+ # in ubuntu, the command is clang-scan-deps-18. in Windows, it is clang-scan-deps
72+ SCAN_DEPS=$( command -v clang-scan-deps-18 || command -v clang-scan-deps)
73+
74+ # Safety check: Exit if the tool isn't found
75+ if [ -z " $SCAN_DEPS " ]; then
76+ echo " Error: clang-scan-deps (or version -18) not found in PATH."
77+ exit 1
78+ fi
79+
80+
81+ # filter compile_commands.json, to remove .rc files, since clang-scan-deps doesn't recognize this format
82+ jq ' [.[] | select(.file | endswith(".rc") | not)]' " $DB_PATH " > " $TMP_DIR /compile_commands_filtered.json"
83+
84+ # get dependency graph
85+ " $SCAN_DEPS " -compilation-database " $TMP_DIR /compile_commands_filtered.json" -format experimental-full > " $TMP_DIR /deps.json"
86+
87+ # normalize slashes
88+ # sed 's|\\\\|/|g' deps.json > normalized_deps.json
89+ sed -i ' s|\\\\|/|g' " $TMP_DIR /deps.json"
90+
91+ # check if deps.json has the expected keys
92+ # because we are relying on clang-scan-deps experimental-full, where the names of the keys can change.
93+ TU_KEY=" translation-units"
94+ CMD_KEY=" commands"
95+ DEPS=" file-deps"
96+ INPUT=" input-file"
97+
98+ JQ_SCRIPT=$( cat << 'EOF '
99+ # 1. Access the target object
100+ (.[$TU][0][$CMD][0]) as $target
101+
102+ # 2. Define the required keys
103+
104+ | [$DEPS, $INPUT] as $required
105+
106+ | (
107+ if .[$TU] == null then
108+ "Missing: \($TU). Keys found at top-level: \(keys_unsorted)"
109+ elif .[$TU][0] == null then
110+ "Missing: \($TU)[0]"
111+ elif .[$TU][0].[$CMD] == null then
112+ "Missing: \($TU)[0].\($CMD). Keys found from \($TU)[0]: \(.[$TU][0] | keys_unsorted)"
113+ elif $target == null then
114+ "Missing: \($TU)[0].[$CMD][0]"
115+ elif ($required | all(. as $req | $target | has($req)) | not) then
116+ "Missing: One or more required keys \($required). Found: \($target | keys_unsorted)"
117+ # elif (.[$TU][0].[$CMD][0] | keys_unsorted | any(. == [$DEPS] or . == [$INPUT]) | not) then
118+ # "Missing: both \($DEPS) and \($INPUT). Found: \(.[$TU][0][$CMD][0] | keys_unsorted)"
119+ else
120+ "All keys \($required) found in \($TU)[0].\($CMD)[0]"
121+ end
122+ ) as $result
123+
124+ | if ($result | type == "string" and startswith("Missing:")) then
125+ ("\($result). The keys within the experimental-full format from clang-scan-deps can change over time. Please fix the CI to use the correct keys.") | halt_error
126+ else $result end
127+ EOF
128+ )
129+
130+ echo " Checking keys in deps.json."
131+ jq -r " $JQ_SCRIPT " \
132+ --arg TU " $TU_KEY " \
133+ --arg CMD " $CMD_KEY " \
134+ --arg DEPS " $DEPS " \
135+ --arg INPUT " $INPUT " \
136+ " $TMP_DIR /deps.json"
137+
138+ echo " Generating changed_files.txt from git diff."
139+
140+ # git diff with relative paths
141+ git diff --name-only origin/main...HEAD > " $TMP_DIR /changed_files.txt"
142+
143+ # if [ "$GITHUB_ACTIONS" == "true" ]; then
144+ # if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
145+ # # Compare PR branch to target branch (main)
146+ # BASE="origin/$GITHUB_BASE_REF"
147+ # else
148+ # # Compare current push to previous state
149+ # BASE="${{ github.event.before }}"
150+ # fi
151+ # HEAD="$GITHUB_SHA"
152+ # else
153+ # # LOCAL: Compare local main to the last known state on server
154+ # # (Assuming you are on main and want to see what you just pushed/changed)
155+ # git fetch origin main
156+ # BASE="origin/main"
157+ # HEAD="HEAD"
158+ # fi
159+
160+ echo " Generating files_to_query.txt, based on changed_files.txt and deps.json."
161+
162+ # for each line in changed_files_unix.txt, search deps.json to find all their dependants
163+ jq -r --rawfile mod " $TMP_DIR /changed_files.txt" \
164+ --arg TU " $TU_KEY " \
165+ --arg CMD " $CMD_KEY " \
166+ --arg DEPS " $DEPS " \
167+ --arg INPUT " $INPUT " '
168+ # 1. Clean the list of changed files
169+ ($mod | split("\n") | map(select(length > 0))) as $changes |
170+
171+ # 2. Access the translation-units array
172+ [ .[$TU][] | .[$CMD][] |
173+ select(
174+ # 3. Check "file-deps" for matches
175+ .[$DEPS][] | . as $dp |
176+ any($changes[]; . as $c | $dp | endswith($c))
177+ ) |
178+ # 4. Get the source file path
179+ .[$INPUT]
180+ ] | unique[]
181+ ' " $TMP_DIR /deps.json" | tr -d ' \r' > " $TMP_DIR /files_to_query.txt"
182+
183+
184+
185+ cat << 'EOF ' > "$TMP_DIR/query.txt"
186+ set output dump
187+ match invocation(
188+ isExpansionInFileMatching("SerialPrograms/"),
189+ hasDeclaration(cxxConstructorDecl(ofClass(hasName("std::filesystem::path")))),
190+ hasArgument(0, hasType(asString("std::string")))
191+ )
192+ EOF
193+
194+ echo " Running clang-query."
195+
196+ # files=$(jq -r '.[].file' "$DB_PATH")
197+ DB_DIR=$( dirname " $DB_PATH " )
198+ # echo "$files" | xargs --max-args=150 clang-query -p "$DB_DIR" -f "$TMP_DIR/query.txt" >> output.txt
199+
200+ # jq -r '.[].file' "$DB_PATH" | sed 's/\\/\//g' | tr -d '\r' | xargs -d '\n' --max-args=150 clang-query -p "$DB_DIR" -f "$TMP_DIR/query.txt" -- -Wno-unused-command-line-argument >> "$TMP_DIR/output.txt"
201+
202+ # this works
203+ # jq -r '.[].file' "$DB_PATH" | sed 's/\\/\//g' | tr -d '\r' | xargs -d '\n' --max-args=150 clang-query -p "$DB_DIR" -f "$TMP_DIR/query.txt" >> "$TMP_DIR/output.txt"
204+ # jq -r '.[].file' "$DB_PATH" | tr -d '\r' | xargs -d '\n' --max-args=150 clang-query -p "$DB_DIR" -f "$TMP_DIR/query.txt" >> "$TMP_DIR/output.txt"
205+
206+ # also works
207+ # jq -r '.[].file' "$DB_PATH" | tr -d '\r' | xargs -d '\n' --max-args=150 \
208+ # clang-query -p "$DB_DIR" \
209+ # --extra-arg="-Wno-unused-command-line-argument" \
210+ # -f "$TMP_DIR/query.txt" >> "$TMP_DIR/output.txt"
211+
212+ # also works
213+ # jq -r '.[].file' "$DB_PATH" | tr -d '\r' | sed 's|\\|/|g' | \
214+ # xargs -d '\n' --max-args=150 \
215+ # clang-query -p "$DB_DIR" \
216+ # --extra-arg="-Wno-unused-command-line-argument" \
217+ # --extra-arg="-Wno-unused-function" \
218+ # -f "$TMP_DIR/query.txt" >> "$TMP_DIR/output.txt"
219+
220+ # in ubuntu, the command is clang-query-18. in Windows, it is clang-query
221+ CLANG_QUERY=$( command -v clang-query-18 || command -v clang-query)
222+
223+ if [ -z " $CLANG_QUERY " ]; then
224+ echo " Error: clang-query (or version -18) not found!"
225+ exit 1
226+ fi
227+
228+ ONLY_CHECK_CHANGED_FILES=true
229+ if [ " $ONLY_CHECK_CHANGED_FILES " = " true" ]; then
230+ LIST_FILE=" $TMP_DIR /files_to_query.txt"
231+ else # check all files
232+ LIST_FILE=" $TMP_DIR /file_list.txt"
233+ jq -r ' .[].file' " $DB_PATH " | tr -d ' \r' | sed ' s|\\|/|g' > " $LIST_FILE "
234+ fi
235+
236+ > " $TMP_DIR /output.txt"
237+
238+ # Run clang-query using the list file
239+ # check if LIST_FILE has any data to analyze
240+ if [ ! -s " $LIST_FILE " ]; then
241+ echo " No files found to analyze. Skipping Clang-Query."
242+ else
243+ xargs -d ' \n' -a " $LIST_FILE " --max-args=150 \
244+ " $CLANG_QUERY " -p " $DB_DIR " \
245+ --extra-arg=" -Wno-unused-command-line-argument" \
246+ --extra-arg=" -Wno-unused-function" \
247+ -f " $TMP_DIR /query.txt" >> " $TMP_DIR /output.txt"
248+ fi
249+
250+
251+
252+ cat " $TMP_DIR /output.txt"
253+ if grep --silent " Match #" " $TMP_DIR /output.txt" ; then
254+ echo " ::error Forbidden std::filesystem::path construction detected!"
255+ exit 1
256+ fi
0 commit comments