From: Yury Norov <ynorov@nvidia.com>
To: Nathan Chancellor <nathan@kernel.org>,
Nicolas Schier <nsc@kernel.org>,
linux-kbuild@vger.kernel.org
Cc: Yury Norov <ynorov@nvidia.com>, Jonathan Corbet <corbet@lwn.net>,
Shuah Khan <skhan@linuxfoundation.org>,
Randy Dunlap <rdunlap@infradead.org>,
Linus Torvalds <torvalds@linux-foundation.org>,
Lorenzo Stoakes <ljs@kernel.org>,
linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 1/2] make: parallelize tags generation
Date: Mon, 14 Sep 2026 20:07:34 -0400 [thread overview]
Message-ID: <20260915000735.1146790-2-ynorov@nvidia.com> (raw)
In-Reply-To: <20260915000735.1146790-1-ynorov@nvidia.com>
Generate per-directory tags in parallel with Exuberant or Universal Ctags.
A recursive make shares the caller's jobserver, allowing make -jN tags to
schedule directory jobs concurrently, then join them to get the traditional
tags file.
Allow compiled-source discovery to find no source references in a batch
of .cmd files, while still propagating grep errors.
The performance of the current vs parallel tags generation with
time make -j8 ALLSOURCE_ARCHS=all tags
is:
real user sys
Before: 4m40.797s 4m14.229s 0m39.341s
After: 1m39.203s 9m1.450s 2m7.552s
Assisted-by: OpenAI Codex
Signed-off-by: Yury Norov <ynorov@nvidia.com>
---
Documentation/kbuild/kbuild.rst | 9 +++
Makefile | 5 +-
scripts/Makefile.tags | 9 +++
scripts/tags.sh | 137 +++++++++++++++++++++++++++++---
4 files changed, 148 insertions(+), 12 deletions(-)
create mode 100644 scripts/Makefile.tags
diff --git a/Documentation/kbuild/kbuild.rst b/Documentation/kbuild/kbuild.rst
index 5a9013bacfb7..61587adeedba 100644
--- a/Documentation/kbuild/kbuild.rst
+++ b/Documentation/kbuild/kbuild.rst
@@ -317,6 +317,15 @@ To get all available archs you can also specify all. E.g.::
$ make ALLSOURCE_ARCHS=all tags
+With Exuberant or Universal Ctags, ``make -jN tags`` generates tags for
+directories in parallel and merges them into a single sorted ``tags`` file.
+The jobs share make's jobserver with other build targets. For example::
+
+ $ make -j8 ALLSOURCE_ARCHS=all tags
+
+Per-directory tag files are temporary and are removed after merging.
+Every invocation regenerates the complete tags file.
+
IGNORE_DIRS
-----------
For tags/TAGS/cscope targets, you can choose which directories won't
diff --git a/Makefile b/Makefile
index 66654fa71655..6f3945fa3fc6 100644
--- a/Makefile
+++ b/Makefile
@@ -2260,7 +2260,10 @@ clean: $(clean-dirs)
quiet_cmd_tags = GEN $@
cmd_tags = $(BASH) $(srctree)/scripts/tags.sh $@
-tags TAGS cscope gtags: FORCE
+tags: FORCE
+ +$(call cmd,tags)
+
+TAGS cscope gtags: FORCE
$(call cmd,tags)
# Generate rust-project.json (a file that describes the structure of non-Cargo
diff --git a/scripts/Makefile.tags b/scripts/Makefile.tags
new file mode 100644
index 000000000000..d5967a3ac496
--- /dev/null
+++ b/scripts/Makefile.tags
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Directory tag shards share the parent make's jobserver.
+
+.PHONY: tags
+tags: $(shell cat $(tags_tmp)/shards)
+ $(Q)$(BASH) $(srctree)/scripts/tags.sh tags-merge $(tags_tmp)
+
+%.tags: %.files
+ $(Q)$(BASH) $(srctree)/scripts/tags.sh tags-worker $@
diff --git a/scripts/tags.sh b/scripts/tags.sh
index 41e38df96984..b001e0f78d2f 100755
--- a/scripts/tags.sh
+++ b/scripts/tags.sh
@@ -121,8 +121,12 @@ all_compiled_sources()
{
{
echo include/generated/autoconf.h
- find $ignore -name "*.cmd" -exec \
- grep -Poh '(?<=^ )\S+\.([chS]|rs)(?=\s)|(?<== )\S+\.(?1)(?=$)' {} \+ |
+ # A .cmd batch with no source references is not an error.
+ find $ignore -name "*.cmd" -exec sh -c '
+ pattern=$1
+ shift
+ grep -Poh "$pattern" "$@" || [ "$?" -eq 1 ]
+ ' sh '(?<=^ )\S+\.([chS]|rs)(?=\s)|(?<== )\S+\.(?1)(?=$)' {} \+ |
awk '!a[$0]++'
} | xargs realpath -esq $([ -z "$KBUILD_ABS_SRCTREE" ] && echo --relative-to=.) |
sort -u
@@ -130,7 +134,9 @@ all_compiled_sources()
all_target_sources()
{
- if [ -n "$COMPILED_SOURCE" ]; then
+ if [ -n "$tags_input" ]; then
+ sed -n 's/^s //p' "$tags_input.files"
+ elif [ -n "$COMPILED_SOURCE" ]; then
all_compiled_sources
else
all_sources
@@ -139,6 +145,10 @@ all_target_sources()
all_kconfigs()
{
+ if [ -n "$tags_input" ]; then
+ sed -n 's/^k //p' "$tags_input.files"
+ return
+ fi
find ${tree}arch/ -maxdepth 1 $ignore \
-name "Kconfig*" -not -type l -print;
for arch in $ALLSOURCE_ARCHS; do
@@ -282,12 +292,24 @@ setup_regex()
done
}
-exuberant()
+setup_ctags()
{
CTAGS_EXTRA="extra"
if $1 --version 2>&1 | grep -iq universal; then
CTAGS_EXTRA="extras"
fi
+ CTAGS_KCONFIG=n
+ if $1 --list-languages | grep -iq kconfig; then
+ CTAGS_KCONFIG=y
+ fi
+ export CTAGS_EXTRA CTAGS_KCONFIG
+}
+
+exuberant()
+{
+ if [ -z "$CTAGS_EXTRA" ]; then
+ setup_ctags "$1"
+ fi
setup_regex exuberant asm c
# identifiers to ignore by ctags
local ign=(
@@ -312,16 +334,75 @@ exuberant()
static
)
all_target_sources | \
- xargs $1 -a -I "$(IFS=','; echo "${ign[*]}")" \
+ xargs -r $1 -a "${tags_flags[@]}" -I "$(IFS=','; echo "${ign[*]}")" \
--$CTAGS_EXTRA=+fq --c-kinds=+px --fields=+iaS --langmap=c:+.h \
- "${regex[@]}"
+ "${regex[@]}" || return
KCONFIG_ARGS=()
- if ! $1 --list-languages | grep -iq kconfig; then
+ if [ "$CTAGS_KCONFIG" != y ]; then
setup_regex exuberant kconfig
KCONFIG_ARGS=(--langdef=kconfig --language-force=kconfig "${regex[@]}")
fi
- all_kconfigs | xargs $1 -a "${KCONFIG_ARGS[@]}"
+ all_kconfigs | xargs -r $1 -a "${tags_flags[@]}" "${KCONFIG_ARGS[@]}"
+}
+
+# Call in a subshell so error handling and cleanup stay local to the operation.
+setup_tags_tmp()
+{
+ set -eo pipefail
+ tmp=$(mktemp -d .tmp_tags.XXXXXX)
+ trap 'rm -rf "$tmp"' EXIT
+ trap 'exit 1' HUP INT TERM
+}
+
+# Let recursive make schedule directory shards using the caller's jobserver.
+parallel_tags()
+(
+ local tmp
+ setup_tags_tmp
+ # Workers inherit these capabilities instead of probing for each directory.
+ setup_ctags ${CTAGS:-ctags}
+
+ {
+ all_target_sources | sed 's/^/s /'
+ all_kconfigs | sed 's/^/k /'
+ } | LC_ALL=C sort -u | awk -v tmp="$tmp" '
+ {
+ dir = substr($0, 3)
+ if (!sub(/\/[^\/]*$/, "", dir))
+ dir = "."
+ if (!(dir in ids)) {
+ ids[dir] = ++n
+ print tmp "/" n ".tags"
+ }
+ out = tmp "/" ids[dir] ".files"
+ if (out != previous) {
+ if (previous != "")
+ close(previous)
+ previous = out
+ }
+ print >> out
+ }' > "$tmp/shards"
+ ${MAKE:-make} -f "${tree}scripts/Makefile.tags" tags_tmp="$tmp"
+)
+
+merge_tags()
+(
+ local tmp
+ setup_tags_tmp
+
+ # Read filenames from stdin to avoid command-line length limits.
+ {
+ tr '\n' '\0' < "$1/shards"
+ # sort requires at least one input, even when there are no shards.
+ printf '/dev/null\0'
+ } | LC_ALL=C sort -m -u --files0-from=- | cut -f2- > "$tmp/merged"
+ mv "$tmp/merged" tags
+)
+
+remove_struct_forward_declarations()
+{
+ LC_ALL=C sed -e '/^\([a-zA-Z_][a-zA-Z0-9_]*\)\t.*\t\/\^struct \1;.*\$\/;"\tx$/d' "$@"
}
emacs()
@@ -366,11 +447,45 @@ case "$1" in
;;
"tags")
- rm -f tags
- xtags ${CTAGS:-ctags}
+ # Recursive recipes also run in dry-run, touch and question modes.
+ # Only normal invocations may generate intermediate files.
+ case ${MAKEFLAGS%% *} in
+ *n*) exit 0 ;;
+ *t*) touch tags; exit $? ;;
+ *q*) exit 1 ;;
+ esac
+ if ${CTAGS:-ctags} --version 2>&1 | grep -Eiq 'exuberant|universal'; then
+ parallel_tags
+ exit $?
+ else
+ rm -f tags
+ xtags ${CTAGS:-ctags}
+ fi
remove_structs=y
;;
+ "tags-worker")
+ tags_input=${2%.tags}
+ # Never expose an incomplete shard after an error or interruption.
+ setup_tags_tmp
+ tags_flags=(-f "$tmp/tags" --sort=no --tag-relative=no)
+ exuberant ${CTAGS:-ctags}
+ # Sort once per changed directory. Prefix records so the final merge
+ # keeps pseudo-tags ahead of all regular tag names.
+ remove_struct_forward_declarations "$tmp/tags" | awk '
+ {
+ sub(/^!_TAG_FILE_SORTED\t0\t/, "!_TAG_FILE_SORTED\t1\t")
+ print (/^!_TAG_/ ? "0\t" : "1\t") $0
+ }' | LC_ALL=C sort --parallel=1 -u > "$tmp/sorted"
+ mv "$tmp/sorted" "$2"
+ exit 0
+ ;;
+
+ "tags-merge")
+ merge_tags "$2"
+ exit $?
+ ;;
+
"TAGS")
rm -f TAGS
xtags etags
@@ -380,5 +495,5 @@ esac
# Remove structure forward declarations.
if [ -n "$remove_structs" ]; then
- LC_ALL=C sed -i -e '/^\([a-zA-Z_][a-zA-Z0-9_]*\)\t.*\t\/\^struct \1;.*\$\/;"\tx$/d' $1
+ remove_struct_forward_declarations -i "$1"
fi
--
2.53.0
next prev parent reply other threads:[~2026-09-15 0:07 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-15 0:07 [PATCH 0/2] make: accelerate tagging process Yury Norov
2026-09-15 0:07 ` Yury Norov [this message]
2026-09-15 0:07 ` [PATCH 2/2] make: cache per-directory tags Yury Norov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260915000735.1146790-2-ynorov@nvidia.com \
--to=ynorov@nvidia.com \
--cc=corbet@lwn.net \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=ljs@kernel.org \
--cc=nathan@kernel.org \
--cc=nsc@kernel.org \
--cc=rdunlap@infradead.org \
--cc=skhan@linuxfoundation.org \
--cc=torvalds@linux-foundation.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®