1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
|
From: Andre Lorbach <alorbach@adiscon.com>
Date: Mon, 20 Oct 2025 12:43:04 +0000
Subject: doc: make git optional for Sphinx builds (dist tarballs)
Building documentation from the dist tarball failed with a RuntimeError
("git executable not found") thrown during Sphinx startup. The helper
module hard-required git even when no .git repo is present.
This change replaces the hard requirement with a safe wrapper that
returns placeholders when git or a repository is unavailable. This lets
Sphinx complete when building from release/dist sources, while keeping
dev builds unchanged.
Details:
- conf_helpers.py: introduce _run_git(); return 'unknown'/'nogit' for
branch/commit and '0.0' for tag-derived versions when git is absent.
- Harden next-version parsing for non-git environments.
- conf.py already guards dev-only dynamic strings behind .git presence.
Fixes: https://github.com/rsyslog/rsyslog/issues/6253
Co-authored-by: alorbach <alorbach@adiscon.com>
(cherry picked from commit 18c8eed4858c4fb5f04018dd8b12148822129069)
---
doc/source/conf_helpers.py | 62 ++++++++++++++++++++++++++++++++--------------
1 file changed, 43 insertions(+), 19 deletions(-)
diff --git a/doc/source/conf_helpers.py b/doc/source/conf_helpers.py
index 70af780..a6aa4e7 100644
--- a/doc/source/conf_helpers.py
+++ b/doc/source/conf_helpers.py
@@ -6,17 +6,34 @@ import shutil
import subprocess
# Resolve git executable to avoid relying on PATH (Bandit B607)
+#
+# Note: Building docs from a release/dist tarball should NOT require git.
+# If git is unavailable or the working directory is not a git repository,
+# we degrade gracefully and return placeholder values so Sphinx can build.
GIT_EXE = shutil.which("git")
-if GIT_EXE is None:
- raise RuntimeError("git executable not found")
+
+
+def _run_git(args):
+ """Run a git command and return decoded stdout, or None on failure.
+
+ We explicitly handle environments where git is missing or the current
+ directory is not a git repository (e.g., dist tarball builds).
+ """
+ if GIT_EXE is None:
+ return None
+ try:
+ output_bytes = subprocess.check_output([GIT_EXE] + args)
+ return output_bytes.decode("utf-8").strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return None
def get_current_branch():
"""Return the current branch we are on or the branch that the detached head
is pointed to"""
- current_branch = subprocess.check_output(
- [GIT_EXE, 'rev-parse', '--abbrev-ref', "HEAD"]
- ).decode("utf-8").strip()
+ current_branch = _run_git(['rev-parse', '--abbrev-ref', 'HEAD'])
+ if not current_branch:
+ return 'unknown'
if current_branch == 'HEAD':
# This means we are operating in a detached head state, will need to
@@ -25,7 +42,10 @@ def get_current_branch():
# Decode "bytes" type to UTF-8 string to avoid Python 3 error:
# "TypeError: a bytes-like object is required, not 'str'""
# https://docs.python.org/3/library/stdtypes.html#bytes.decode
- branches = subprocess.check_output([GIT_EXE, 'branch']).decode('utf-8').split('\n')
+ branches_output = _run_git(['branch'])
+ if not branches_output:
+ return 'unknown'
+ branches = branches_output.split('\n')
for branch in branches:
# Git marks the current branch, or in this case the branch
@@ -49,9 +69,9 @@ def get_current_stable_version():
def get_latest_tag():
""""Helper function: Return the latest git tag"""
- git_tag_output = subprocess.check_output(
- [GIT_EXE, 'tag', '--list', "v*"]
- ).decode("utf-8").strip()
+ git_tag_output = _run_git(['tag', '--list', 'v*'])
+ if not git_tag_output:
+ return None
git_tag_list = re.sub('[A-Za-z]', '', git_tag_output).split('\n')
git_tag_list.sort(key=lambda s: [int(u) for u in s.split('.')])
@@ -62,6 +82,9 @@ def get_current_stable_version():
return git_tag_latest
latest_tag = get_latest_tag()
+ if not latest_tag:
+ # Fallback for non-git environments (e.g., release tarball builds)
+ return '0.0'
# Return 'X.Y' from 'X.Y.Z'
return latest_tag[:-2]
@@ -73,10 +96,14 @@ def get_next_stable_version():
current_version = get_current_stable_version()
# Break apart 'x.y' value, increment y and then concatenate into 'x.y' again
- next_version = "{}.{}".format(
- int(current_version[:1]),
- int(current_version[-2:]) + 1
+ try:
+ next_version = "{}.{}".format(
+ int(current_version.split('.')[0]),
+ int(current_version.split('.')[1]) + 1
)
+ except (IndexError, ValueError):
+ # Conservative fallback if parsing fails
+ next_version = '0.1'
return next_version
@@ -84,9 +111,8 @@ def get_next_stable_version():
def get_current_commit_hash():
"""Return commit hash string"""
- commit_hash = subprocess.check_output([GIT_EXE, 'log', '--pretty=format:%h', 'HEAD', '-n1']).decode("utf-8")
-
- return commit_hash
+ commit_hash = _run_git(['log', '--pretty=format:%h', 'HEAD', '-n1'])
+ return commit_hash if commit_hash else 'nogit'
def get_release_string(release_type, release_string_detail, version):
@@ -108,9 +134,7 @@ def get_release_string(release_type, release_string_detail, version):
# 'rsyslog' prefix is already set via 'project' variable in conf.py
# HASH
# 'docs' string suffix is already ...
- release_string = "{}".format(
- get_current_commit_hash()
- )
+ release_string = "{}".format(get_current_commit_hash())
elif release_string_detail == "detailed":
# The verbose variation of the release string. This was previously
@@ -122,7 +146,7 @@ def get_release_string(release_type, release_string_detail, version):
get_current_branch(),
TODAY,
get_current_commit_hash()
- )
+ )
else:
# This means that someone set a value that we do not
# have a format defined for. Return an error string instead
|