From 5486c70aae94196d715c07364292fc108ebf59a8 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Tue, 28 Nov 2023 16:38:07 +1100 Subject: [PATCH] Fix #115056: man-page fails to build with non-portable install This effectively reverts [0] which required the installed Blender along with the `bpy` module to load for the install target to generate the man-page. It turns out running a full Blender at install time can be quite involved. Now the man-page generator runs `blender --help` & `blender --version`, converting the output into a man-page from a Python script. Some minor changes have also been made: - Use PYTHON_EXECUTABLE so the systems Python (which may not be compatible) isn't used. - Remove leading blender version from the description which was unintentionally being included. [0]: 61d99d450ee1d55e2829dc6adbef2cf8a094989b --- doc/manpage/blender.1.py | 92 +++++++++++++++++++++++++++-------- source/creator/CMakeLists.txt | 10 ++-- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/doc/manpage/blender.1.py b/doc/manpage/blender.1.py index 0f86e505937..e29afd2c58c 100755 --- a/doc/manpage/blender.1.py +++ b/doc/manpage/blender.1.py @@ -7,7 +7,7 @@ This script generates the blender.1 man page, embedding the help text from the Blender executable itself. Invoke it as follows: - ./blender.bin -b --python doc/manpage/blender.1.py -- --output + blender.1.py --blender --output where is the path to the Blender executable, and is where to write the generated man page. @@ -15,8 +15,8 @@ and is where to write the generated man page. import argparse import os +import subprocess import time -import sys from typing import ( Dict, @@ -30,28 +30,77 @@ def man_format(data: str) -> str: return data -def blender_extract_info() -> Dict[str, str]: - # Only use of `bpy` in this file. - import bpy # type: ignore - blender_help_text = bpy.app.help_text() - blender_version_text = bpy.app.version_string - blender_build_date_text = bpy.app.build_date +def blender_extract_info(blender_bin: str) -> Dict[str, str]: + blender_env = { + "ASAN_OPTIONS": ( + os.environ.get("ASAN_OPTIONS", "") + + ":exitcode=0:check_initialization_order=0:strict_init_order=0" + ).lstrip(":"), + } - if blender_build_date_text == b'Unknown': + # NOTE: in some ways it's more elegant to use `bpy.app.help_text()` which was done but had to be reverted. + # however - this requires Blender to run with a full environment (initializing it's Python environment). + # See #115056 & !115320 for details. + + blender_help = subprocess.run( + [blender_bin, "--help"], + env=blender_env, + check=True, + stdout=subprocess.PIPE, + ).stdout.decode(encoding="utf-8") + + blender_version_output = subprocess.run( + [blender_bin, "--version"], + env=blender_env, + check=True, + stdout=subprocess.PIPE, + ).stdout.decode(encoding="utf-8") + + # Extract information from the version string. + # Note that some internal modules may print errors (e.g. color management), + # check for each lines prefix to ensure these aren't included. + blender_version = "" + blender_date = "" + + # The full text (use to manipulate `blender_version_text`). + blender_version_text = "" + + for l in blender_version_output.split("\n"): + if l.startswith("Blender "): + if blender_version_text == "": + blender_version_text = l + # Remove `Blender` prefix. + blender_version = l.split(" ", 1)[1].strip() + elif l.lstrip().startswith("build date:"): + # Remove `build date:` prefix. + blender_date = l.split(":", 1)[1].strip() + if blender_version and blender_date: + break + + # The `--help` text also contains the version, skip it so as not to include it twice. + if blender_version_text: + i = blender_help.find(blender_version_text) + if i != -1: + blender_help = blender_help[i + len(blender_version_text) + 1:] + del i + + if not blender_date: # Happens when built without WITH_BUILD_INFO e.g. - blender_date = time.strftime("%B %d, %Y", time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))) + date_string = time.strftime("%B %d, %Y", time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))) else: - blender_date = time.strftime("%B %d, %Y", time.strptime(blender_build_date_text.decode(), "%Y-%m-%d")) + date_string = time.strftime("%B %d, %Y", time.strptime(blender_date, "%Y-%m-%d")) return { - "help": blender_help_text, - "version": blender_version_text, - "date": blender_date, + "help": blender_help, + "version": blender_version, + "date": date_string, } -def man_page_from_blender_help(fh: TextIO, verbose: bool) -> None: - blender_info = blender_extract_info() +def man_page_from_blender_help(fh: TextIO, blender_bin: str, verbose: bool) -> None: + if verbose: + print("Extracting help text:", blender_bin) + blender_info = blender_extract_info(blender_bin) # Header Content. fh.write( @@ -144,6 +193,11 @@ def create_argparse() -> argparse.ArgumentParser: required=True, help="The man page to write to." ) + parser.add_argument( + "--blender", + required=True, + help="Path to the blender binary." + ) parser.add_argument( "--verbose", default=False, @@ -156,15 +210,15 @@ def create_argparse() -> argparse.ArgumentParser: def main() -> None: - argv = sys.argv[sys.argv.index("--") + 1:] parser = create_argparse() - args = parser.parse_args(argv) + args = parser.parse_args() output_filename = args.output + blender_bin = args.blender verbose = args.verbose with open(output_filename, "w", encoding="utf-8") as fh: - man_page_from_blender_help(fh, verbose) + man_page_from_blender_help(fh, blender_bin, verbose) if verbose: print("Written:", output_filename) diff --git a/source/creator/CMakeLists.txt b/source/creator/CMakeLists.txt index 9ec40f1d4e8..6c4363bad58 100644 --- a/source/creator/CMakeLists.txt +++ b/source/creator/CMakeLists.txt @@ -1805,6 +1805,7 @@ if(UNIX AND NOT APPLE) install( CODE "\ set(BLENDER_BIN \"${CMAKE_INSTALL_PREFIX}/${BLENDER_BIN}\")\n\ +set(PYTHON_EXECUTABLE \"${PYTHON_EXECUTABLE}\")\n\ set(MANPAGE_GEN \"${CMAKE_SOURCE_DIR}/doc/manpage/blender.1.py\")\n\ set(MANPAGE_OUT \"${CMAKE_CURRENT_BINARY_DIR}/blender.1\")\n\ if(\n\ @@ -1818,12 +1819,9 @@ strict_init_order=0:\ detect_leaks=0\"\n\ )\n\ execute_process(\n\ - OUTPUT_QUIET\n\ - COMMAND $\{BLENDER_BIN\}\n\ - --background\n\ - --factory-startup\n\ - --python $\{MANPAGE_GEN\}\n\ - --\n\ + COMMAND\n\ + $\{PYTHON_EXECUTABLE\} $\{MANPAGE_GEN\}\n\ + --blender $\{BLENDER_BIN\}\n\ --output $\{MANPAGE_OUT\}\n\ )\n\ endif()\n\