@@ -16,7 +16,7 @@ trap 's=$?; echo >&2 "$0: error on line "${LINENO}": ${BASH_COMMAND}"; exit ${s}
1616# - shellcheck
1717# - npm
1818# - jq
19- # - python
19+ # - python 3
2020# - rustup (if Rust code exists)
2121# - clang-format (if C/C++ code exists)
2222#
@@ -40,6 +40,19 @@ check_config() {
4040 error " could not found $1 in the repository root"
4141 fi
4242}
43+ check_install () {
44+ for tool in " $@ " ; do
45+ if ! type -P " ${tool} " & > /dev/null; then
46+ if [[ " ${tool} " == " python3" ]]; then
47+ if type -P python & > /dev/null; then
48+ continue
49+ fi
50+ fi
51+ error " '${tool} ' is required to run this check"
52+ return 1
53+ fi
54+ done
55+ }
4356info () {
4457 echo >&2 " info: $* "
4558}
@@ -56,6 +69,27 @@ venv() {
5669 shift
5770 " ${venv_bin} /${bin}${exe} " " $@ "
5871}
72+ venv_install_yq () {
73+ local py_suffix=' '
74+ if type -P python3 & > /dev/null; then
75+ py_suffix=' 3'
76+ fi
77+ exe=' '
78+ venv_bin=' .venv/bin'
79+ case " $( uname -s) " in
80+ MINGW* | MSYS* | CYGWIN* | Windows_NT)
81+ exe=' .exe'
82+ venv_bin=' .venv/Scripts'
83+ ;;
84+ esac
85+ if [[ ! -d .venv ]]; then
86+ " python${py_suffix} " -m venv .venv
87+ fi
88+ if [[ ! -e " ${venv_bin} /yq${exe} " ]]; then
89+ info " installing yq to ./.venv using pip"
90+ venv " pip${py_suffix} " install yq
91+ fi
92+ }
5993
6094if [[ $# -gt 0 ]]; then
6195 cat << EOF
68102# Rust (if exists)
69103if [[ -n " $( git ls-files ' *.rs' ) " ]]; then
70104 info " checking Rust code style"
105+ check_install cargo jq python3
71106 check_config .rustfmt.toml
72- if type -P rustup & > /dev/null ; then
107+ if check_install rustup; then
73108 # `cargo fmt` cannot recognize files not included in the current workspace and modules
74109 # defined inside macros, so run rustfmt directly.
75110 # We need to use nightly rustfmt because we use the unstable formatting options of rustfmt.
76111 rustc_version=$( rustc -vV | grep ' ^release:' | cut -d' ' -f2)
77112 if [[ " ${rustc_version} " == * " nightly" * ]] || [[ " ${rustc_version} " == * " dev" * ]]; then
78113 rustup component add rustfmt & > /dev/null
79- echo " + rustfmt \$ (git ls-files '*.rs')"
114+ info " running \` rustfmt \$ (git ls-files '*.rs')\` "
80115 rustfmt $( git ls-files ' *.rs' )
81116 else
82117 rustup component add rustfmt --toolchain nightly & > /dev/null
83- echo " + rustfmt +nightly \$ (git ls-files '*.rs')"
118+ info " running \` rustfmt +nightly \$ (git ls-files '*.rs')\` "
84119 rustfmt +nightly $( git ls-files ' *.rs' )
85120 fi
86121 check_diff $( git ls-files ' *.rs' )
87- else
88- error " 'rustup' is not installed; skipped Rust code style check"
89122 fi
90123 cast_without_turbofish=$( grep -n -E ' \.cast\(\)' $( git ls-files ' *.rs' ) || true)
91124 if [[ -n " ${cast_without_turbofish} " ]]; then
@@ -122,11 +155,12 @@ if [[ -n "$(git ls-files '*.rs')" ]]; then
122155 binaries=' '
123156 metadata=$( cargo metadata --format-version=1 --no-deps)
124157 has_public_crate=' '
158+ venv_install_yq
125159 for id in $( jq <<< " ${metadata}" ' .workspace_members[]' ) ; do
126160 pkg=$( jq <<< " ${metadata}" " .packages[] | select(.id == ${id} )" )
127161 publish=$( jq <<< " ${pkg}" -r ' .publish' )
128162 manifest_path=$( jq <<< " ${pkg}" -r ' .manifest_path' )
129- if ! grep -q ' ^\[lints\] ' " ${manifest_path} " && ! grep -q ' ^\[lints\.rust\] ' " ${manifest_path} " ; then
163+ if [[ " $( venv tomlq -c ' .lints ' " ${manifest_path} " ) " == " null " ]] ; then
130164 error " no [lints] table in ${manifest_path} please add '[lints]' with 'workspace = true'"
131165 fi
132166 # Publishing is unrestricted if null, and forbidden if an empty array.
@@ -144,13 +178,14 @@ if [[ -n "$(git ls-files '*.rs')" ]]; then
144178 publish=$( jq <<< " ${root_pkg}" -r ' .publish' )
145179 # Publishing is unrestricted if null, and forbidden if an empty array.
146180 if [[ " ${publish} " != " []" ]]; then
147- if ! grep -Eq ' ^exclude = \[.*"/\.\*".*\]' Cargo.toml; then
181+ exclude=$( venv tomlq -r ' .package.exclude[]' Cargo.toml)
182+ if ! grep <<< " ${exclude}" -Eq ' ^/\.\*$' ; then
148183 error " top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \" /.*\" "
149184 fi
150- if [[ -e tools ]] && ! grep -Eq ' ^exclude = \[.*" /tools".*\] ' Cargo.toml ; then
185+ if [[ -e tools ]] && ! grep <<< " ${exclude} " -Eq ' ^/tools$ ' ; then
151186 error " top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \" /tools\" if it exists"
152187 fi
153- if [[ -e target-specs ]] && ! grep -Eq ' ^exclude = \[.*" /target-specs".*\] ' Cargo.toml ; then
188+ if [[ -e target-specs ]] && ! grep <<< " ${exclude} " -Eq ' ^/target-specs$ ' ; then
154189 error " top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \" /target-specs\" if it exists"
155190 fi
156191 fi
196231if [[ -n " $( git ls-files ' *.c' ' *.h' ' *.cpp' ' *.hpp' ) " ]]; then
197232 info " checking C/C++ code style"
198233 check_config .clang-format
199- if type -P clang-format & > /dev/null ; then
200- echo " + clang-format -i \$ (git ls-files '*.c' '*.h' '*.cpp' '*.hpp')"
234+ if check_install clang-format; then
235+ info " running \` clang-format -i \$ (git ls-files '*.c' '*.h' '*.cpp' '*.hpp')\` "
201236 clang-format -i $( git ls-files ' *.c' ' *.h' ' *.cpp' ' *.hpp' )
202237 check_diff $( git ls-files ' *.c' ' *.h' ' *.cpp' ' *.hpp' )
203- else
204- error " 'clang-format' is not installed; skipped C/C++ code style check"
205238 fi
206239elif [[ -e .clang-format ]]; then
207240 error " .clang-format is unused"
211244if [[ -n " $( git ls-files ' *.yml' ' *.yaml' ' *.js' ' *.json' ) " ]]; then
212245 info " checking YAML/JavaScript/JSON code style"
213246 check_config .editorconfig
214- if type -P npm & > /dev/null ; then
215- echo " + npx -y prettier -l -w \$ (git ls-files '*.yml' '*.yaml' '*.js' '*.json')"
247+ if check_install npm; then
248+ info " running \` npx -y prettier -l -w \$ (git ls-files '*.yml' '*.yaml' '*.js' '*.json')\` "
216249 npx -y prettier -l -w $( git ls-files ' *.yml' ' *.yaml' ' *.js' ' *.json' )
217250 check_diff $( git ls-files ' *.yml' ' *.yaml' ' *.js' ' *.json' )
218- else
219- error " 'npm' is not installed; skipped YAML/JavaScript/JSON code style check"
220251 fi
221252 # Check GitHub workflows.
222253 if [[ -d .github/workflows ]]; then
223254 info " checking GitHub workflows"
224- if type -P jq & > /dev/null; then
225- if type -P python3 & > /dev/null || type -P python & > /dev/null; then
226- py_suffix=' '
227- if type -P python3 & > /dev/null; then
228- py_suffix=' 3'
229- fi
230- exe=' '
231- venv_bin=' .venv/bin'
232- case " $( uname -s) " in
233- MINGW* | MSYS* | CYGWIN* | Windows_NT)
234- exe=' .exe'
235- venv_bin=' .venv/Scripts'
236- ;;
255+ if check_install jq python3; then
256+ venv_install_yq
257+ for workflow in .github/workflows/* .yml; do
258+ # The top-level permissions must be weak as they are referenced by all jobs.
259+ permissions=$( venv yq -c ' .permissions' " ${workflow} " )
260+ case " ${permissions} " in
261+ ' {"contents":"read"}' | ' {"contents":"none"}' ) ;;
262+ null) error " ${workflow} : top level permissions not found; it must be 'contents: read' or weaker permissions" ;;
263+ * ) error " ${workflow} : only 'contents: read' and weaker permissions are allowed at top level; if you want to use stronger permissions, please set job-level permissions" ;;
237264 esac
238- if [[ ! -d .venv ]]; then
239- " python${py_suffix} " -m venv .venv
240- fi
241- if [[ ! -e " ${venv_bin} /yq${exe} " ]]; then
242- venv " pip${py_suffix} " install yq
243- fi
244- for workflow in .github/workflows/* .yml; do
245- # The top-level permissions must be weak as they are referenced by all jobs.
246- permissions=$( venv yq -c ' .permissions' " ${workflow} " )
247- case " ${permissions} " in
248- ' {"contents":"read"}' | ' {"contents":"none"}' ) ;;
249- null) error " ${workflow} : top level permissions not found; it must be 'contents: read' or weaker permissions" ;;
250- * ) error " ${workflow} : only 'contents: read' and weaker permissions are allowed at top level; if you want to use stronger permissions, please set job-level permissions" ;;
251- esac
252- # Make sure the 'needs' section is not out of date.
253- if grep -q ' # tidy:needs' " ${workflow} " && ! grep -Eq ' # *needs: \[' " ${workflow} " ; then
254- # shellcheck disable=SC2207
255- jobs_actual=($( venv yq ' .jobs' " ${workflow} " | jq -r ' keys_unsorted[]' ) )
256- unset ' jobs_actual[${#jobs_actual[@]}-1]'
257- # shellcheck disable=SC2207
258- jobs_expected=($( venv yq -r ' .jobs."ci-success".needs[]' " ${workflow} " ) )
259- if [[ " ${jobs_actual[*]} " != " ${jobs_expected[*]+" ${jobs_expected[*]} " } " ]]; then
260- printf -v jobs ' %s, ' " ${jobs_actual[@]} "
261- sed -i " s/needs: \[.*\] # tidy:needs/needs: [${jobs% , } ] # tidy:needs/" " ${workflow} "
262- check_diff " ${workflow} "
263- error " ${workflow} : please update 'needs' section in 'ci-success' job"
264- fi
265+ # Make sure the 'needs' section is not out of date.
266+ if grep -q ' # tidy:needs' " ${workflow} " && ! grep -Eq ' # *needs: \[' " ${workflow} " ; then
267+ # shellcheck disable=SC2207
268+ jobs_actual=($( venv yq ' .jobs' " ${workflow} " | jq -r ' keys_unsorted[]' ) )
269+ unset ' jobs_actual[${#jobs_actual[@]}-1]'
270+ # shellcheck disable=SC2207
271+ jobs_expected=($( venv yq -r ' .jobs."ci-success".needs[]' " ${workflow} " ) )
272+ if [[ " ${jobs_actual[*]} " != " ${jobs_expected[*]+" ${jobs_expected[*]} " } " ]]; then
273+ printf -v jobs ' %s, ' " ${jobs_actual[@]} "
274+ sed -i " s/needs: \[.*\] # tidy:needs/needs: [${jobs% , } ] # tidy:needs/" " ${workflow} "
275+ check_diff " ${workflow} "
276+ error " ${workflow} : please update 'needs' section in 'ci-success' job"
265277 fi
266- done
267- else
268- error " 'python3' is not installed; skipped GitHub workflow check"
269- fi
270- else
271- error " 'jq' is not installed; skipped GitHub workflow check"
278+ fi
279+ done
272280 fi
273281 fi
274282fi
281289if [[ -n " $( git ls-files ' *.toml' | (grep -v .taplo.toml || true)) " ]]; then
282290 info " checking TOML style"
283291 check_config .taplo.toml
284- if type -P npm & > /dev/null ; then
285- echo " + npx -y @taplo/cli fmt \$ (git ls-files '*.toml')"
292+ if check_install npm; then
293+ info " running \` npx -y @taplo/cli fmt \$ (git ls-files '*.toml')\` "
286294 RUST_LOG=warn npx -y @taplo/cli fmt $( git ls-files ' *.toml' )
287295 check_diff $( git ls-files ' *.toml' )
288- else
289- error " 'npm' is not installed; skipped TOML style check"
290296 fi
291297elif [[ -e .taplo.toml ]]; then
292298 error " .taplo.toml is unused"
296302if [[ -n " $( git ls-files ' *.md' ) " ]]; then
297303 info " checking Markdown style"
298304 check_config .markdownlint-cli2.yaml
299- if type -P npm & > /dev/null ; then
300- echo " + npx -y markdownlint-cli2 \$ (git ls-files '*.md')"
305+ if check_install npm; then
306+ info " running \` npx -y markdownlint-cli2 \$ (git ls-files '*.md')\` "
301307 npx -y markdownlint-cli2 $( git ls-files ' *.md' )
302- else
303- error " 'npm' is not installed; skipped Markdown style check"
304308 fi
305309elif [[ -e .markdownlint-cli2.yaml ]]; then
306310 error " .markdownlint-cli2.yaml is unused"
312316
313317# Shell scripts
314318info " checking Shell scripts"
315- if type -P shfmt & > /dev/null ; then
319+ if check_install shfmt; then
316320 check_config .editorconfig
317- echo " + shfmt -l -w \$ (git ls-files '*.sh')"
321+ info " running \` shfmt -l -w \$ (git ls-files '*.sh')\` "
318322 shfmt -l -w $( git ls-files ' *.sh' )
319323 check_diff $( git ls-files ' *.sh' )
320- else
321- error " 'shfmt' is not installed; skipped Shell scripts style check"
322324fi
323- if type -P shellcheck & > /dev/null ; then
325+ if check_install shellcheck; then
324326 check_config .shellcheckrc
325- echo " + shellcheck \$ (git ls-files '*.sh')"
327+ info " running \` shellcheck \$ (git ls-files '*.sh')\` "
326328 if ! shellcheck $( git ls-files ' *.sh' ) ; then
327329 should_fail=1
328330 fi
329331 if [[ -n " $( git ls-files ' *Dockerfile' ) " ]]; then
330332 # SC2154 doesn't seem to work on dockerfile.
331- echo " + shellcheck -e SC2148,SC2154,SC2250 \$ (git ls-files '*Dockerfile')"
333+ info " running \` shellcheck -e SC2148,SC2154,SC2250 \$ (git ls-files '*Dockerfile')\` "
332334 if ! shellcheck -e SC2148,SC2154,SC2250 $( git ls-files ' *Dockerfile' ) ; then
333335 should_fail=1
334336 fi
335337 fi
336- else
337- error " 'shellcheck' is not installed; skipped Shell scripts style check"
338338fi
339339
340340# License check
383383if [[ -f .cspell.json ]]; then
384384 info " spell checking"
385385 project_dictionary=.github/.cspell/project-dictionary.txt
386- if type -P npm & > /dev/null ; then
386+ if check_install npm jq python3 ; then
387387 has_rust=' '
388388 if [[ -n " $( git ls-files ' *Cargo.toml' ) " ]]; then
389+ venv_install_yq
389390 has_rust=' 1'
390391 dependencies=' '
391392 for manifest_path in $( git ls-files ' *Cargo.toml' ) ; do
392- if [[ " ${manifest_path} " != " Cargo.toml" ]] && ! grep -Eq ' \[ workspace\] ' " ${manifest_path} " ; then
393+ if [[ " ${manifest_path} " != " Cargo.toml" ]] && [[ " $( venv tomlq -c ' . workspace' " ${manifest_path} " ) " == " null " ]] ; then
393394 continue
394395 fi
395396 metadata=$( cargo metadata --format-version=1 --no-deps --manifest-path " ${manifest_path} " )
422423 error " you may want to mark .github/.cspell/rust-dependencies.txt linguist-generated"
423424 fi
424425
425- echo " + npx -y cspell --no-progress --no-summary \$ (git ls-files)"
426+ info " running \` npx -y cspell --no-progress --no-summary \$ (git ls-files)\` "
426427 if ! npx -y cspell --no-progress --no-summary $( git ls-files) ; then
427428 error " spellcheck failed: please fix uses of above words or add to ${project_dictionary} if correct"
428429 fi
454455 echo -n " ${unused} "
455456 echo " ======================================="
456457 fi
457- else
458- error " 'npm' is not installed; skipped spell check"
459458 fi
460459fi
461460
0 commit comments