Add Chromium-only Blender WebEngine parity work
This commit is contained in:
307
blender-5.2.0/tools/utils_maintenance/autopep8_format_paths.py
Executable file
307
blender-5.2.0/tools/utils_maintenance/autopep8_format_paths.py
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This script runs autopep8 on multiple files/directories.
|
||||
|
||||
While it can be called directly, you may prefer to run this from Blender's root directory with the command:
|
||||
|
||||
make format
|
||||
|
||||
Otherwise you may call this script directly, for example:
|
||||
|
||||
./tools/utils_maintenance/autopep8_format_paths.py --changed-only tests/python
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import subprocess
|
||||
import argparse
|
||||
|
||||
VERSION_MIN = (2, 3, 1)
|
||||
VERSION_MAX_RECOMMENDED = (2, 3, 1)
|
||||
AUTOPEP8_FORMAT_CMD = "autopep8"
|
||||
AUTOPEP8_FORMAT_DEFAULT_ARGS = (
|
||||
# Operate on all directories recursively.
|
||||
"--recursive",
|
||||
# Update the files in-place.
|
||||
"--in-place",
|
||||
# Auto-detect the number of jobs to use.
|
||||
"--jobs=0",
|
||||
)
|
||||
|
||||
BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
os.chdir(BASE_DIR)
|
||||
|
||||
|
||||
extensions = (
|
||||
".py",
|
||||
)
|
||||
|
||||
ignore_files = {
|
||||
"scripts/modules/_rna_manual_reference.py", # Large generated file, don't format.
|
||||
"tools/svn_rev_map/rev_to_sha1.py",
|
||||
"tools/svn_rev_map/sha1_to_rev.py",
|
||||
}
|
||||
|
||||
|
||||
def compute_paths(paths: list[str], use_default_paths: bool) -> list[str]:
|
||||
# Optionally pass in files to operate on.
|
||||
if use_default_paths:
|
||||
paths = [
|
||||
"build_files",
|
||||
"intern",
|
||||
"release",
|
||||
"scripts",
|
||||
"doc",
|
||||
"source",
|
||||
"tests",
|
||||
"tools",
|
||||
]
|
||||
else:
|
||||
paths = [
|
||||
f for f in paths
|
||||
if os.path.isdir(f) or (os.path.isfile(f) and f.endswith(extensions))
|
||||
]
|
||||
|
||||
if os.sep != "/":
|
||||
paths = [f.replace("/", os.sep) for f in paths]
|
||||
return paths
|
||||
|
||||
|
||||
def source_files_from_git(paths: list[str], changed_only: bool) -> list[str]:
|
||||
if changed_only:
|
||||
cmd = ("git", "diff", "HEAD", "--name-only", "-z", "--", *paths)
|
||||
else:
|
||||
cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z")
|
||||
files = subprocess.check_output(cmd).split(b'\0')
|
||||
return [f.decode('utf-8') for f in files]
|
||||
|
||||
|
||||
def autopep8_parse_version(version: str) -> tuple[int, int, int]:
|
||||
# Ensure exactly 3 numbers.
|
||||
major, minor, patch = (tuple(int(n) for n in version.split("-")[0].split(".")) + (0, 0, 0))[0:3]
|
||||
return major, minor, patch
|
||||
|
||||
|
||||
def version_str_from_tuple(version: tuple[int, ...]) -> str:
|
||||
return ".".join(str(x) for x in version)
|
||||
|
||||
|
||||
def autopep8_ensure_version_from_command(
|
||||
autopep8_format_cmd_argument: str,
|
||||
) -> tuple[str, tuple[int, int, int]] | None:
|
||||
|
||||
# The version to parse.
|
||||
version_str: str | None = None
|
||||
|
||||
global AUTOPEP8_FORMAT_CMD
|
||||
autopep8_format_cmd = None
|
||||
version_output = None
|
||||
# Attempt to use `--autopep8-command` passed in from `make format`
|
||||
# so the autopep8 distributed with Blender will be used.
|
||||
for is_default in (True, False):
|
||||
if is_default:
|
||||
autopep8_format_cmd = autopep8_format_cmd_argument
|
||||
if autopep8_format_cmd and os.path.exists(autopep8_format_cmd):
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
autopep8_format_cmd = "autopep8"
|
||||
|
||||
cmd = [autopep8_format_cmd]
|
||||
if cmd[0].endswith(".py"):
|
||||
cmd = [sys.executable, *cmd]
|
||||
|
||||
try:
|
||||
version_output = subprocess.check_output((*cmd, "--version")).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
AUTOPEP8_FORMAT_CMD = autopep8_format_cmd
|
||||
break
|
||||
|
||||
if version_output is not None:
|
||||
version_str = next(iter(v for v in version_output.split() if v[0].isdigit()), None)
|
||||
|
||||
if version_str is not None:
|
||||
assert isinstance(autopep8_format_cmd, str)
|
||||
major, minor, patch = autopep8_parse_version(version_str)
|
||||
return autopep8_format_cmd, (major, minor, patch)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def autopep8_ensure_version_from_module() -> tuple[str, tuple[int, int, int]] | None:
|
||||
|
||||
# The version to parse.
|
||||
version_str: str | None = None
|
||||
|
||||
# Extract the version from the module.
|
||||
try:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import autopep8 # type: ignore
|
||||
except ModuleNotFoundError as ex:
|
||||
if ex.name != "autopep8":
|
||||
raise ex
|
||||
return None
|
||||
|
||||
version_str = autopep8.__version__
|
||||
if version_str is not None:
|
||||
major, minor, patch = autopep8_parse_version(version_str)
|
||||
return autopep8.__file__, (major, minor, patch)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def autopep8_format(files: list[str]) -> bytes:
|
||||
cmd = [
|
||||
AUTOPEP8_FORMAT_CMD,
|
||||
*AUTOPEP8_FORMAT_DEFAULT_ARGS,
|
||||
*files
|
||||
]
|
||||
|
||||
# Support executing from the module directory because Blender does not distribute the command.
|
||||
if cmd[0].endswith(".py"):
|
||||
cmd = [sys.executable, *cmd]
|
||||
|
||||
return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def autopep8_format_no_subprocess(files: list[str]) -> None:
|
||||
cmd = [
|
||||
*AUTOPEP8_FORMAT_DEFAULT_ARGS,
|
||||
*files
|
||||
]
|
||||
# NOTE: this import will have already succeeded, see: `autopep8_ensure_version_from_module`.
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from autopep8 import main as autopep8_main
|
||||
autopep8_main(argv=cmd)
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Format Python source code.",
|
||||
epilog=__doc__,
|
||||
# Don't re-wrap text, keep newlines & indentation.
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--changed-only",
|
||||
dest="changed_only",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=(
|
||||
"Format only edited files, including the staged ones. "
|
||||
"Using this with \"paths\" will pick the edited files lying on those paths. "
|
||||
"(default=False)"
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-subprocess",
|
||||
dest="no_subprocess",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=(
|
||||
"Don't use a sub-process, load autopep8 into this instance of Python. "
|
||||
"Works around 8191 argument length limit on WIN32."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--autopep8-command",
|
||||
dest="autopep8_command",
|
||||
default=AUTOPEP8_FORMAT_CMD,
|
||||
help="The command to call autopep8.",
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="All trailing arguments are treated as paths.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
if args.no_subprocess:
|
||||
source_and_version = autopep8_ensure_version_from_module()
|
||||
else:
|
||||
source_and_version = autopep8_ensure_version_from_command(args.autopep8_command)
|
||||
|
||||
if source_and_version is None:
|
||||
if args.no_subprocess:
|
||||
sys.stderr.write("ERROR: unable to import \"autopep8\" from Python at \"{:s}\".\n".format(sys.executable))
|
||||
else:
|
||||
sys.stderr.write("ERROR: unable to detect \"autopep8 --version\".\n")
|
||||
|
||||
sys.stderr.write("You may want to install autopep8-{:s}, or use the pre-compiled libs repository.\n".format(
|
||||
version_str_from_tuple(VERSION_MAX_RECOMMENDED[0:2]),
|
||||
))
|
||||
return 1
|
||||
|
||||
autopep8_source, version = source_and_version
|
||||
|
||||
if version < VERSION_MIN:
|
||||
sys.stderr.write("Using \"{:s}\"\nERROR: the autopep8 version is too old: {:s} < {:s}.\n".format(
|
||||
autopep8_source,
|
||||
version_str_from_tuple(version),
|
||||
version_str_from_tuple(VERSION_MIN),
|
||||
))
|
||||
return 1
|
||||
if version > VERSION_MAX_RECOMMENDED:
|
||||
sys.stderr.write("Using \"{:s}\"\nWARNING: the autopep8 version is too recent: {:s} > {:s}.\n".format(
|
||||
autopep8_source,
|
||||
version_str_from_tuple(version),
|
||||
version_str_from_tuple(VERSION_MAX_RECOMMENDED),
|
||||
))
|
||||
sys.stderr.write("You may want to install autopep8-{:s}, or use the pre-compiled libs repository.\n".format(
|
||||
version_str_from_tuple(VERSION_MAX_RECOMMENDED[0:2]),
|
||||
))
|
||||
else:
|
||||
print("Using \"{:s}\", ({:s})...".format(autopep8_source, version_str_from_tuple(version)))
|
||||
|
||||
use_default_paths = not (bool(args.paths) or bool(args.changed_only))
|
||||
paths = compute_paths(args.paths, use_default_paths)
|
||||
# Check if user-defined paths exclude all Python sources.
|
||||
if args.paths and not paths:
|
||||
print("Skip autopep8: no target to format")
|
||||
return 0
|
||||
|
||||
print("Operating on:" + (" ({:d} changed paths)".format(len(paths)) if args.changed_only else ""))
|
||||
for p in paths:
|
||||
print(" ", p)
|
||||
|
||||
files = [
|
||||
f for f in source_files_from_git(paths, args.changed_only)
|
||||
if f.endswith(extensions)
|
||||
if f not in ignore_files
|
||||
]
|
||||
|
||||
# Happens when users run "make format" passing in individual C/C++ files
|
||||
# (and no Python files).
|
||||
if not files:
|
||||
return 0
|
||||
|
||||
if args.no_subprocess:
|
||||
autopep8_format_no_subprocess(files)
|
||||
else:
|
||||
autopep8_format(files)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,668 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Example usage:
|
||||
#
|
||||
# ./blender.bin --factory-startup \
|
||||
# --enable-event-simulate \
|
||||
# --python tools/utils_maintenance/blender_menu_search_coverage.py
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import bpy
|
||||
|
||||
# Menu-ID -> class.
|
||||
MENU_TYPE_MAP = {}
|
||||
|
||||
# Track which menus were called per-context,
|
||||
# this ensures that menus were called at all.
|
||||
MENU_CALLED_RUNTIME = set()
|
||||
|
||||
# Use for adding mouse movement (refreshing).
|
||||
EVENT_ARGS_NOP = dict(type='MOUSEMOVE', value='NOTHING')
|
||||
|
||||
# List of shell-glob expressions to ignore.
|
||||
OPERATOR_IGNORE = (
|
||||
"action.clickselect",
|
||||
"action.select_lasso",
|
||||
"armature.select_linked_pick",
|
||||
"armature.shortest_path_pick",
|
||||
"buttons.context_menu",
|
||||
"buttons.directory_browse",
|
||||
"buttons.file_browse",
|
||||
"clip.cursor_set", # Interactive cursor placement.
|
||||
"clip.select_lasso",
|
||||
"console.*",
|
||||
"curve.draw",
|
||||
"curve.select_linked_pick",
|
||||
"ed.undo_redo", # The UI exposes undo/redo operators.
|
||||
"file.bookmark_add",
|
||||
"file.bookmark_cleanup",
|
||||
"file.bookmark_delete",
|
||||
"file.bookmark_move",
|
||||
"file.cancel",
|
||||
"file.delete",
|
||||
"file.directory_new",
|
||||
"file.execute",
|
||||
"file.filenum",
|
||||
"file.filepath_drop",
|
||||
"file.hidedot",
|
||||
"file.highlight",
|
||||
"file.next",
|
||||
"file.parent",
|
||||
"file.previous",
|
||||
"file.refresh",
|
||||
"file.rename",
|
||||
"file.reset_recent",
|
||||
"file.select",
|
||||
"file.select_bookmark",
|
||||
"file.select_walk",
|
||||
"file.smoothscroll",
|
||||
"font.line_break",
|
||||
"font.move",
|
||||
"gizmogroup.gizmo_select",
|
||||
"gizmogroup.gizmo_tweak",
|
||||
"gpencil.draw", # Interactive drawing.
|
||||
"gpencil.select_lasso",
|
||||
"gpencil.vertex_paint",
|
||||
"gpencil.weight_paint",
|
||||
"graph.click_insert",
|
||||
"graph.clickselect",
|
||||
"graph.cursor_set", # Interactive cursor placement.
|
||||
"graph.select_lasso",
|
||||
"mask.select_lasso",
|
||||
"mask.select_linked_pick",
|
||||
"mesh.polybuild_*", # Only accessed via tool.
|
||||
"mesh.primitive_cube_add_gizmo", # Only accessed via tool.
|
||||
"mesh.rip",
|
||||
"mesh.rip_edge",
|
||||
"mesh.select_linked_pick",
|
||||
"mesh.shortest_path_pick", # Uses mouse.
|
||||
"node.select_lasso",
|
||||
"object.add_named",
|
||||
"object.material_slot_move",
|
||||
"object.mode_set",
|
||||
"object.mode_set_with_submode",
|
||||
"object.posemode_toggle",
|
||||
"paint.face_select_linked_pick",
|
||||
"paint.weight_sample",
|
||||
"paint.weight_sample_group",
|
||||
"paintcurve.draw", # Interactive drawing.
|
||||
"particle.select_linked_pick",
|
||||
"pose.select_linked_pick",
|
||||
"preferences.addon_disable",
|
||||
"preferences.addon_enable",
|
||||
"preferences.addon_install",
|
||||
"preferences.addon_refresh",
|
||||
"preferences.addon_remove",
|
||||
"preferences.copy_prev",
|
||||
"preferences.keyconfig_activate",
|
||||
"preferences.keyconfig_export",
|
||||
"preferences.keyconfig_import",
|
||||
"preferences.keyconfig_remove",
|
||||
"preferences.keyconfig_test",
|
||||
"preferences.keyitem_add",
|
||||
"preferences.keyitem_remove",
|
||||
"preferences.keyitem_restore",
|
||||
"preferences.keymap_restore",
|
||||
"preferences.reset_default_theme",
|
||||
"preferences.studiolight_copy_settings",
|
||||
"preferences.studiolight_install",
|
||||
"preferences.studiolight_new",
|
||||
"preferences.studiolight_uninstall",
|
||||
"preferences.theme_install",
|
||||
"scene.delete",
|
||||
"scene.light_cache_bake",
|
||||
"scene.light_cache_free",
|
||||
"scene.new",
|
||||
"scene.render_view_add",
|
||||
"scene.render_view_remove",
|
||||
"screen.animation_cancel",
|
||||
"screen.animation_step",
|
||||
"screen.area_swap",
|
||||
"screen.back_to_previous",
|
||||
"screen.delete",
|
||||
"screen.drivers_editor_show",
|
||||
"screen.frame_jump",
|
||||
"screen.frame_offset",
|
||||
"screen.header_toggle_menus",
|
||||
"screen.new",
|
||||
"screen.region_context_menu",
|
||||
"screen.region_flip",
|
||||
"screen.region_toggle",
|
||||
"screen.screen_set",
|
||||
"screen.space_context_cycle",
|
||||
"screen.space_type_set_or_cycle", # Only makes sense from key binding.
|
||||
"script.execute_preset",
|
||||
"sequencer.select_linked_pick",
|
||||
"text.cursor_set", # Interactive cursor placement.
|
||||
"text.find", # text.start_find
|
||||
"text.indent_or_autocomplete",
|
||||
"text.insert",
|
||||
"text.line_break",
|
||||
"text.replace",
|
||||
"text.replace_set_selected",
|
||||
"text.resolve_conflict",
|
||||
"text.selection_set",
|
||||
"ui.*",
|
||||
"uv.rip",
|
||||
"uv.select",
|
||||
"uv.select_edge_ring",
|
||||
"uv.select_lasso",
|
||||
"uv.select_linked_pick",
|
||||
"uv.select_loop",
|
||||
"uv.shortest_path_pick",
|
||||
"view2d.scroll_down",
|
||||
"view2d.scroll_left",
|
||||
"view2d.scroll_right",
|
||||
"view2d.scroll_up",
|
||||
"view2d.scroller_activate",
|
||||
"view3d.cursor3d",
|
||||
"view3d.select",
|
||||
"view3d.select_lasso",
|
||||
"view3d.select_menu",
|
||||
"view3d.view_center_pick",
|
||||
"wm.doc_view",
|
||||
"wm.doc_view_manual",
|
||||
"wm.doc_view_manual_ui_context",
|
||||
"wm.owner_disable",
|
||||
"wm.owner_enable",
|
||||
"wm.radial_control",
|
||||
"wm.search_operator", # Only for users who prefer this behavior.
|
||||
"wm.tool_set_by_id",
|
||||
"wm.tool_set_by_index",
|
||||
"wm.toolbar",
|
||||
"wm.toolbar_fallback_pie",
|
||||
"wm.toolbar_prompt",
|
||||
"wm.window_close",
|
||||
"workspace.*",
|
||||
|
||||
)
|
||||
|
||||
# Operators found in menus.
|
||||
OPERATOR_FOUND = set()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generate Operator List
|
||||
#
|
||||
|
||||
|
||||
def operator_list():
|
||||
"""
|
||||
Filter this, allowing is to ignore some operators.
|
||||
"""
|
||||
# Filter the list.
|
||||
from fnmatch import fnmatchcase
|
||||
|
||||
def is_op_ok(op):
|
||||
for op_match in OPERATOR_IGNORE:
|
||||
if fnmatchcase(op, op_match):
|
||||
print(" skipping: {:s} ({:s})".format(op, op_match))
|
||||
return False
|
||||
return True
|
||||
|
||||
operators = []
|
||||
for mod_name in dir(bpy.ops):
|
||||
mod = getattr(bpy.ops, mod_name)
|
||||
for submod_name in dir(mod):
|
||||
op = getattr(mod, submod_name)
|
||||
bl_options = op.bl_options
|
||||
if 'INTERNAL' in bl_options:
|
||||
continue
|
||||
|
||||
op_id = "{:s}.{:s}".format(mod_name, submod_name)
|
||||
if not is_op_ok(op_id):
|
||||
continue
|
||||
|
||||
operators.append((op_id, op))
|
||||
operators.sort(key=lambda op_pair: op_pair[0])
|
||||
return operators
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Setup Functions
|
||||
|
||||
def setup_contants():
|
||||
from bpy.types import Menu
|
||||
for cls in Menu.__subclasses__():
|
||||
if cls.is_registered:
|
||||
bl_idname = getattr(cls, "bl_idname", cls.__name__)
|
||||
MENU_TYPE_MAP[bl_idname] = cls
|
||||
|
||||
|
||||
def setup_menu_wrap_draw_call_all():
|
||||
|
||||
def operators_from_layout_introspect(layout_introspect):
|
||||
assert isinstance(layout_introspect, list)
|
||||
for item in layout_introspect:
|
||||
value = item.get("items")
|
||||
if value is not None:
|
||||
assert isinstance(value, list)
|
||||
yield from operators_from_layout_introspect(value)
|
||||
value = item.get("operator")
|
||||
if value is not None:
|
||||
assert isinstance(value, str)
|
||||
# We don't need the arguments at the moment.
|
||||
assert value.startswith("bpy.ops.")
|
||||
yield value[8:].split("(")[0]
|
||||
|
||||
def menu_draw_introspect(self, _context):
|
||||
bl_idname = getattr(cls, "bl_idname", type(self).__name__)
|
||||
layout_introspect = self.layout.introspect()
|
||||
for op_id in operators_from_layout_introspect(layout_introspect):
|
||||
OPERATOR_FOUND.add(op_id)
|
||||
MENU_CALLED_RUNTIME.add(bl_idname)
|
||||
|
||||
# Instead of monkey patching the draw function, use the built-in `Menu.append` method,
|
||||
# which allows us to access the layout which has been created so far.
|
||||
#
|
||||
# This works as long as this is the last append call to the menu
|
||||
# (add-ons will have already loaded).
|
||||
for cls in MENU_TYPE_MAP.values():
|
||||
cls.append(menu_draw_introspect)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Simulate Events
|
||||
|
||||
def run_event_simulate(event_iter):
|
||||
TICKS = 1
|
||||
|
||||
def event_step():
|
||||
# print("timer:", event_step._ticks)
|
||||
# Run once 'TICKS' is reached.
|
||||
if event_step._ticks < TICKS:
|
||||
event_step._ticks += 1
|
||||
return 0.0
|
||||
|
||||
event_step._ticks = 0
|
||||
|
||||
val = next(event_step.run_events, Ellipsis)
|
||||
if val is Ellipsis:
|
||||
bpy.app.use_event_simulate = False
|
||||
print("Finished simulation")
|
||||
return None
|
||||
|
||||
# Run event simulation.
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
if "x" not in val:
|
||||
val["x"] = win.width // 2
|
||||
if "y" not in val:
|
||||
val["y"] = win.height // 2
|
||||
|
||||
# Fake event value, since press, release is so common.
|
||||
if val.get("value") == 'TAP':
|
||||
del val["value"]
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
win.event_simulate(**val, value='PRESS')
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
win.event_simulate(**val, value='RELEASE')
|
||||
else:
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
win.event_simulate(**val)
|
||||
return 0.0
|
||||
|
||||
event_step.run_events = iter(event_iter)
|
||||
event_step._ticks = 0
|
||||
|
||||
bpy.app.timers.register(event_step, first_interval=1.0, persistent=True)
|
||||
|
||||
|
||||
def setup_default_preferences(preferences):
|
||||
""" Set preferences useful for automation.
|
||||
"""
|
||||
preferences.view.show_splash = False
|
||||
preferences.view.smooth_view = 0
|
||||
preferences.view.use_save_prompt = False
|
||||
preferences.view.show_developer_ui = True
|
||||
preferences.filepaths.use_auto_save_temporary_files = False
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Context Setup
|
||||
|
||||
# -------
|
||||
# Default
|
||||
|
||||
def ctx_objectmode_default():
|
||||
pass
|
||||
|
||||
# ----
|
||||
# Text
|
||||
|
||||
|
||||
def ctx_text_default():
|
||||
found = False
|
||||
text = bpy.data.texts.new(name="Text")
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'TEXT_EDITOR':
|
||||
area.spaces.active.text = text
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
# -----
|
||||
# Image
|
||||
|
||||
def ctx_image_view_default():
|
||||
found = False
|
||||
image = bpy.data.images.new(name="Image", width=1, height=1)
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'IMAGE_EDITOR':
|
||||
space_data = area.spaces.active
|
||||
space_data.image = image
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
def ctx_image_view_render():
|
||||
found = False
|
||||
image = bpy.data.images["Render Result"]
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'IMAGE_EDITOR':
|
||||
space_data = area.spaces.active
|
||||
space_data.image = image
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
def ctx_image_mask_default():
|
||||
found = False
|
||||
mask = bpy.data.masks.new(name="Mask")
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'IMAGE_EDITOR':
|
||||
space_data = area.spaces.active
|
||||
space_data.mode = 'MASK'
|
||||
space_data.mask = mask
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
def ctx_image_paint_default():
|
||||
found = False
|
||||
image = bpy.data.images.new(name="Image", width=1, height=1)
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'IMAGE_EDITOR':
|
||||
space_data = area.spaces.active
|
||||
space_data.mode = 'PAINT'
|
||||
space_data.image = image
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
# ----
|
||||
# Clip
|
||||
|
||||
def ctx_clip_default():
|
||||
found = False
|
||||
# Load '.' is a trick so we don't need to read a real image.
|
||||
clip = bpy.data.movieclips.load(filepath=".")
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == 'CLIP_EDITOR':
|
||||
area.spaces.active.clip = clip
|
||||
found = True
|
||||
assert found
|
||||
|
||||
|
||||
# ----------
|
||||
# Edit Modes
|
||||
|
||||
def ctx_editmode_mesh():
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_mesh_extra():
|
||||
bpy.ops.object.vertex_group_add()
|
||||
bpy.ops.object.shape_key_add(from_mix=False) # Basis Key
|
||||
shape_key = bpy.ops.object.shape_key_add(from_mix=True)
|
||||
shape_key.value = 0.0
|
||||
bpy.ops.mesh.uv_texture_add()
|
||||
bpy.ops.mesh.vertex_color_add()
|
||||
bpy.ops.object.material_slot_add()
|
||||
# Edit-mode last!
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_curve():
|
||||
bpy.ops.curve.primitive_nurbs_circle_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_surface():
|
||||
bpy.ops.surface.primitive_nurbs_surface_torus_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_mball():
|
||||
bpy.ops.object.metaball_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_text():
|
||||
bpy.ops.object.text_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_armature():
|
||||
bpy.ops.object.armature_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
def ctx_editmode_armature_empty():
|
||||
bpy.ops.object.armature_add()
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.armature.select_all(action='SELECT')
|
||||
bpy.ops.armature.delete()
|
||||
|
||||
|
||||
def ctx_editmode_lattice():
|
||||
bpy.ops.object.add(type='LATTICE')
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
# bpy.ops.object.vertex_group_add()
|
||||
|
||||
|
||||
def ctx_object_empty():
|
||||
bpy.ops.object.add(type='EMPTY')
|
||||
|
||||
|
||||
# -------------
|
||||
# Grease Pencil
|
||||
|
||||
|
||||
def ctx_gpencil_edit():
|
||||
bpy.ops.object.gpencil_add(type='STROKE')
|
||||
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||
|
||||
|
||||
def ctx_gpencil_sculpt():
|
||||
bpy.ops.object.grease_pencil_add(type='STROKE')
|
||||
bpy.ops.object.mode_set(mode='SCULPT_GREASE_PENCIL')
|
||||
|
||||
|
||||
def ctx_gpencil_paint_weight():
|
||||
bpy.ops.object.grease_pencil_add(type='STROKE')
|
||||
bpy.ops.object.mode_set(mode='WEIGHT_GREASE_PENCIL')
|
||||
|
||||
|
||||
def ctx_gpencil_paint_vertex():
|
||||
bpy.ops.object.grease_pencil_add(type='STROKE')
|
||||
bpy.ops.object.mode_set(mode='VERTEX_GREASE_PENCIL')
|
||||
|
||||
|
||||
def ctx_gpencil_paint_draw():
|
||||
bpy.ops.object.grease_pencil_add(type='STROKE')
|
||||
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
|
||||
|
||||
# ------------
|
||||
# Object Modes
|
||||
|
||||
def ctx_object_pose():
|
||||
bpy.ops.object.armature_add()
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
bpy.ops.pose.select_all(action='SELECT')
|
||||
|
||||
|
||||
def ctx_object_particle_edit():
|
||||
bpy.ops.object.quick_fur()
|
||||
bpy.ops.object.mode_set(mode='PARTICLE_EDIT')
|
||||
|
||||
|
||||
def ctx_object_volume():
|
||||
bpy.ops.object.add(type='VOLUME')
|
||||
|
||||
|
||||
# -----------
|
||||
# Paint Modes
|
||||
|
||||
def ctx_object_paint_weight():
|
||||
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
|
||||
|
||||
|
||||
def ctx_object_paint_weight_with_vert_mask():
|
||||
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
|
||||
for mesh in bpy.data.meshes:
|
||||
mesh.use_paint_mask_vertex = True
|
||||
|
||||
|
||||
def ctx_object_paint_vertex():
|
||||
bpy.ops.object.mode_set(mode='VERTEX_PAINT')
|
||||
|
||||
|
||||
def ctx_object_paint_sculpt():
|
||||
bpy.ops.object.mode_set(mode='SCULPT')
|
||||
|
||||
|
||||
def ctx_object_paint_texture():
|
||||
bpy.ops.object.mode_set(mode='TEXTURE_PAINT')
|
||||
|
||||
|
||||
def ctx_object_paint_texture_with_face_mask():
|
||||
bpy.ops.object.mode_set(mode='TEXTURE_PAINT')
|
||||
for mesh in bpy.data.meshes:
|
||||
mesh.use_paint_mask = True
|
||||
|
||||
|
||||
def perform_coverage_test():
|
||||
def open_and_close_menu_search():
|
||||
MENU_CALLED_RUNTIME.clear()
|
||||
yield dict(type='F3', value='TAP')
|
||||
yield dict(type='ESC', value='TAP')
|
||||
assert len(MENU_CALLED_RUNTIME) != 0
|
||||
|
||||
operators_pairs_all = operator_list()
|
||||
|
||||
for ctx_fn, space_ui_type in (
|
||||
(ctx_objectmode_default, 'VIEW_3D'),
|
||||
|
||||
(ctx_object_particle_edit, 'VIEW_3D'),
|
||||
(ctx_object_pose, 'VIEW_3D'),
|
||||
(ctx_object_volume, 'VIEW_3D'),
|
||||
|
||||
# Edit modes.
|
||||
(ctx_editmode_armature, 'VIEW_3D'),
|
||||
(ctx_editmode_curve, 'VIEW_3D'),
|
||||
(ctx_editmode_lattice, 'VIEW_3D'),
|
||||
(ctx_editmode_mball, 'VIEW_3D'),
|
||||
(ctx_editmode_mesh, 'VIEW_3D'),
|
||||
(ctx_editmode_mesh_extra, 'VIEW_3D'),
|
||||
(ctx_editmode_surface, 'VIEW_3D'),
|
||||
(ctx_editmode_text, 'VIEW_3D'),
|
||||
|
||||
# Paint modes.
|
||||
(ctx_object_paint_sculpt, 'VIEW_3D'),
|
||||
(ctx_object_paint_texture, 'VIEW_3D'),
|
||||
(ctx_object_paint_texture_with_face_mask, 'VIEW_3D'),
|
||||
(ctx_object_paint_vertex, 'VIEW_3D'),
|
||||
(ctx_object_paint_weight, 'VIEW_3D'),
|
||||
(ctx_object_paint_weight_with_vert_mask, 'VIEW_3D'),
|
||||
|
||||
# Grease pencil modes.
|
||||
(ctx_gpencil_edit, 'VIEW_3D'),
|
||||
(ctx_gpencil_paint_draw, 'VIEW_3D'),
|
||||
(ctx_gpencil_paint_vertex, 'VIEW_3D'),
|
||||
(ctx_gpencil_paint_weight, 'VIEW_3D'),
|
||||
(ctx_gpencil_sculpt, 'VIEW_3D'),
|
||||
|
||||
# Other spaces.
|
||||
(ctx_clip_default, 'CLIP_EDITOR'),
|
||||
(ctx_editmode_mesh, 'UV'),
|
||||
(ctx_image_mask_default, 'IMAGE_EDITOR'),
|
||||
(ctx_image_paint_default, 'IMAGE_EDITOR'),
|
||||
(ctx_image_view_default, 'IMAGE_EDITOR'),
|
||||
(ctx_image_view_render, 'IMAGE_EDITOR'),
|
||||
|
||||
(ctx_text_default, 'INFO'),
|
||||
(ctx_text_default, 'TEXT_EDITOR'),
|
||||
|
||||
(ctx_objectmode_default, 'CONSOLE'),
|
||||
(ctx_objectmode_default, 'CompositorNodeTree'),
|
||||
(ctx_objectmode_default, 'DOPESHEET'),
|
||||
(ctx_objectmode_default, 'DRIVERS'),
|
||||
(ctx_objectmode_default, 'FCURVES'),
|
||||
(ctx_objectmode_default, 'FILE_BROWSER'),
|
||||
(ctx_objectmode_default, 'INFO'),
|
||||
(ctx_objectmode_default, 'NLA_EDITOR'),
|
||||
(ctx_objectmode_default, 'OUTLINER'),
|
||||
(ctx_objectmode_default, 'PREFERENCES'),
|
||||
(ctx_objectmode_default, 'PROPERTIES'),
|
||||
(ctx_objectmode_default, 'SEQUENCE_EDITOR'),
|
||||
(ctx_objectmode_default, 'ShaderNodeTree'),
|
||||
(ctx_objectmode_default, 'TIMELINE'),
|
||||
(ctx_objectmode_default, 'TextureNodeTree'),
|
||||
):
|
||||
bpy.ops.wm.read_homefile(use_empty=False, use_factory_startup=True)
|
||||
# Set view full-screen.
|
||||
yield dict(type='SPACE', value='TAP', ctrl=True)
|
||||
if space_ui_type != 'VIEW_3D':
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
win.screen.areas[0].ui_type = space_ui_type
|
||||
# import IPython; IPython.embed()
|
||||
yield EVENT_ARGS_NOP
|
||||
|
||||
ctx_fn()
|
||||
yield from open_and_close_menu_search()
|
||||
|
||||
operators_all = {op_pair[0] for op_pair in operators_pairs_all}
|
||||
|
||||
# The menu might use some internal operators, that's fine but
|
||||
# could give confusing percentages.
|
||||
operators_menu = (operators_all & OPERATOR_FOUND)
|
||||
|
||||
len_op = len(operators_all)
|
||||
len_op_menu = len(operators_menu)
|
||||
|
||||
for op in sorted(operators_all - operators_menu):
|
||||
print(op)
|
||||
|
||||
# Report:
|
||||
print(
|
||||
"Coverage {:.2f} ({:d} of {:d})".format(
|
||||
(len_op_menu / len_op) * 100.0,
|
||||
len_op_menu,
|
||||
len_op,
|
||||
))
|
||||
|
||||
# Quit!
|
||||
yield Ellipsis
|
||||
|
||||
|
||||
def main():
|
||||
setup_default_preferences(bpy.context.preferences)
|
||||
setup_contants()
|
||||
setup_menu_wrap_draw_call_all()
|
||||
|
||||
run_event_simulate(perform_coverage_test())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# this script updates XML themes once new settings are added
|
||||
#
|
||||
# ./blender.bin --background --python ./tools/utils_maintenance/blender_update_themes.py
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import bpy
|
||||
import os
|
||||
|
||||
|
||||
def update(filepath):
|
||||
import _rna_xml as rna_xml
|
||||
context = bpy.context
|
||||
|
||||
print("Updating theme: {!r}".format(filepath))
|
||||
preset_xml_map = (
|
||||
("preferences.themes[0]", "Theme"),
|
||||
("preferences.ui_styles[0]", "Theme"),
|
||||
)
|
||||
rna_xml.xml_file_run(
|
||||
context,
|
||||
filepath,
|
||||
preset_xml_map,
|
||||
)
|
||||
|
||||
rna_xml.xml_file_write(
|
||||
context,
|
||||
filepath,
|
||||
preset_xml_map,
|
||||
)
|
||||
|
||||
|
||||
def update_default(filepath):
|
||||
with open(filepath, 'w', encoding='utf-8') as fh:
|
||||
fh.write("""<bpy>
|
||||
<Theme>
|
||||
</Theme>
|
||||
<ThemeStyle>
|
||||
</ThemeStyle>
|
||||
</bpy>
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
for path in bpy.utils.preset_paths("interface_theme"):
|
||||
for fn in os.listdir(path):
|
||||
if fn.endswith(".xml"):
|
||||
fn_full = os.path.join(path, fn)
|
||||
if fn == "blender_dark.xml":
|
||||
update_default(fn_full)
|
||||
else:
|
||||
update(fn_full)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
105
blender-5.2.0/tools/utils_maintenance/c_sort_blocks.py
Executable file
105
blender-5.2.0/tools/utils_maintenance/c_sort_blocks.py
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
PWD = os.path.dirname(__file__)
|
||||
sys.path.append(os.path.join(PWD, "modules"))
|
||||
|
||||
from batch_edit_text import run
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(PWD, "..", ".."))))
|
||||
|
||||
# TODO, move to config file
|
||||
SOURCE_DIRS = (
|
||||
"source",
|
||||
"intern/ghost",
|
||||
)
|
||||
|
||||
SOURCE_EXT = (
|
||||
# C/C++
|
||||
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
|
||||
# Objective C
|
||||
".m", ".mm",
|
||||
)
|
||||
|
||||
|
||||
def sort_struct_lists(fn: str, data_src: str) -> str | None:
|
||||
import re
|
||||
|
||||
# Disable for now.
|
||||
use_datatoc_match = False
|
||||
|
||||
# eg:
|
||||
# struct Foo;
|
||||
re_match_struct = re.compile(r"struct\s+[A-Za-z_][A-Za-z_0-9]*\s*;")
|
||||
# eg:
|
||||
# struct Foo Bar;
|
||||
re_match_struct_type = re.compile(r"struct\s+[A-Za-z_][A-Za-z_0-9]*\s+[A-Za-z_][A-Za-z_0-9]*\s*;")
|
||||
|
||||
# typedef struct Foo Bar;
|
||||
re_match_typedef_struct_type = re.compile(
|
||||
r"typedef\s+struct\s+[A-Za-z_][A-Za-z_0-9]*\s+[A-Za-z_][A-Za-z_0-9]*\s*;")
|
||||
|
||||
re_match_enum = re.compile(r"enum\s+[A-Za-z_][A-Za-z_0-9]*\s*;")
|
||||
|
||||
if use_datatoc_match:
|
||||
# eg:
|
||||
# `extern char datatoc_splash_png[];`
|
||||
re_match_datatoc = re.compile(r"extern\s+(char)\s+datatoc_[A-Za-z_].*;")
|
||||
|
||||
lines = data_src.splitlines(keepends=True)
|
||||
|
||||
def can_sort(l: str) -> int | None:
|
||||
if re_match_struct.match(l):
|
||||
return 1
|
||||
if re_match_struct_type.match(l):
|
||||
return 2
|
||||
if re_match_typedef_struct_type.match(l):
|
||||
return 3
|
||||
if re_match_enum.match(l):
|
||||
return 4
|
||||
if use_datatoc_match:
|
||||
if re_match_datatoc.match(l):
|
||||
return 5
|
||||
return None
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
i_type = can_sort(lines[i])
|
||||
if i_type is not None:
|
||||
j = i
|
||||
while j + 1 < len(lines):
|
||||
if can_sort(lines[j + 1]) != i_type:
|
||||
break
|
||||
j = j + 1
|
||||
if i != j:
|
||||
lines[i:j + 1] = list(sorted(lines[i:j + 1]))
|
||||
i = j
|
||||
i = i + 1
|
||||
|
||||
data_dst = "".join(lines)
|
||||
if data_src != data_dst:
|
||||
return data_dst
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
run(
|
||||
directories=[os.path.join(SOURCE_DIR, d) for d in SOURCE_DIRS],
|
||||
is_text=lambda fn: fn.endswith(SOURCE_EXT),
|
||||
text_operation=sort_struct_lists,
|
||||
use_multiprocess=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
102
blender-5.2.0/tools/utils_maintenance/c_struct_clean.py
Executable file
102
blender-5.2.0/tools/utils_maintenance/c_struct_clean.py
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
When a source file declares a struct which isn't used anywhere else in the file.
|
||||
Remove it.
|
||||
|
||||
There may be times this is needed, however they can typically be removed
|
||||
and any errors caused can be added to the headers which require the forward declarations.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
PWD = os.path.dirname(__file__)
|
||||
sys.path.append(os.path.join(PWD, "modules"))
|
||||
|
||||
from batch_edit_text import run
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(PWD, "..", ".."))))
|
||||
|
||||
# TODO: move to configuration file.
|
||||
SOURCE_DIRS = (
|
||||
"source",
|
||||
os.path.join("intern", "ghost"),
|
||||
)
|
||||
|
||||
SOURCE_EXT = (
|
||||
# C/C++
|
||||
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
|
||||
# Objective C
|
||||
".m", ".mm",
|
||||
)
|
||||
|
||||
re_words = re.compile("[A-Za-z_][A-Za-z_0-9]*")
|
||||
re_match_struct = re.compile(r"struct\s+([A-Za-z_][A-Za-z_0-9]*)\s*;")
|
||||
|
||||
|
||||
def clean_structs(fn: str, data_src: str) -> str | None:
|
||||
from pygments.token import Token
|
||||
from pygments import lexers
|
||||
|
||||
word_occurance: dict[str, int] = {}
|
||||
|
||||
lex = lexers.get_lexer_by_name("c++")
|
||||
lex.get_tokens(data_src)
|
||||
|
||||
ty_exact = (Token.Comment.Preproc, Token.Comment.PreprocFile)
|
||||
|
||||
for ty, _text in lex.get_tokens(data_src):
|
||||
if ty not in ty_exact:
|
||||
if ty in Token.String: # type: ignore
|
||||
continue
|
||||
if ty in Token.Comment: # type: ignore
|
||||
continue
|
||||
|
||||
for w_match in re_words.finditer(data_src):
|
||||
w = w_match.group(0)
|
||||
try:
|
||||
word_occurance[w] += 1
|
||||
except KeyError:
|
||||
word_occurance[w] = 1
|
||||
|
||||
lines = data_src.splitlines(keepends=True)
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
m = re_match_struct.match(lines[i])
|
||||
if m is not None:
|
||||
struct_name = m.group(1)
|
||||
if word_occurance[struct_name] == 1:
|
||||
print(struct_name, fn)
|
||||
del lines[i]
|
||||
i -= 1
|
||||
|
||||
i += 1
|
||||
|
||||
data_dst = "".join(lines)
|
||||
if data_src != data_dst:
|
||||
return data_dst
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
run(
|
||||
directories=[os.path.join(SOURCE_DIR, d) for d in SOURCE_DIRS],
|
||||
is_text=lambda fn: fn.endswith(SOURCE_EXT),
|
||||
text_operation=clean_structs,
|
||||
use_multiprocess=False,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
287
blender-5.2.0/tools/utils_maintenance/clang_format_paths.py
Executable file
287
blender-5.2.0/tools/utils_maintenance/clang_format_paths.py
Executable file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This script runs clang-format on multiple files/directories.
|
||||
|
||||
While it can be called directly, you may prefer to run this from Blender's root directory with the command:
|
||||
|
||||
make format
|
||||
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from collections.abc import (
|
||||
Sequence,
|
||||
)
|
||||
|
||||
VERSION_MIN = (20, 1, 8)
|
||||
VERSION_MAX_RECOMMENDED = (20, 1, 8)
|
||||
CLANG_FORMAT_CMD = "clang-format"
|
||||
|
||||
BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
os.chdir(BASE_DIR)
|
||||
|
||||
|
||||
extensions = (
|
||||
".c", ".cc", ".cpp", ".cxx",
|
||||
".h", ".hh", ".hpp", ".hxx",
|
||||
".m", ".mm",
|
||||
".osl", ".glsl", ".msl",
|
||||
".metal",
|
||||
)
|
||||
|
||||
extensions_only_retab = (
|
||||
".cmake",
|
||||
"CMakeLists.txt",
|
||||
".sh",
|
||||
)
|
||||
|
||||
# Add files which are too large/heavy to format.
|
||||
ignore_files: set[str] = set([
|
||||
# Currently empty, looks like.
|
||||
# "intern/cycles/render/sobol.cpp",
|
||||
])
|
||||
|
||||
# Directories not to format (recursively).
|
||||
#
|
||||
# Notes:
|
||||
# - These directories must also have a `.clang-format` that disables formatting,
|
||||
# so developers who use format-on-save functionality enabled don't have these files formatted on save.
|
||||
# - The reason to exclude here is to prevent unnecessary work were the files would run through clang-format
|
||||
# only to do nothing because the `.clang-format` file prevents it.
|
||||
ignore_directories = {
|
||||
"intern/itasc"
|
||||
}
|
||||
|
||||
|
||||
def compute_paths(paths: list[str], use_default_paths: bool) -> list[str]:
|
||||
# The resulting paths:
|
||||
# - Use forward slashes on all systems.
|
||||
# - Are relative to the GIT repository without any `.` or `./` prefix.
|
||||
|
||||
# Optionally pass in files to operate on.
|
||||
if use_default_paths:
|
||||
paths = [
|
||||
"intern",
|
||||
"source",
|
||||
"tests/gtests",
|
||||
]
|
||||
else:
|
||||
# Filter out files, this is only done so this utility wont print that it's
|
||||
# "Operating" on files that will be filtered out later on.
|
||||
paths = [
|
||||
f for f in paths
|
||||
if os.path.isdir(f) or (os.path.isfile(f) and f.endswith(extensions))
|
||||
]
|
||||
|
||||
if os.sep != "/":
|
||||
paths = [f.replace("/", os.sep) for f in paths]
|
||||
return paths
|
||||
|
||||
|
||||
def source_files_from_git(paths: Sequence[str], changed_only: bool) -> list[str]:
|
||||
if changed_only:
|
||||
cmd = ("git", "diff", "HEAD", "--name-only", "-z", "--", *paths)
|
||||
else:
|
||||
cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z")
|
||||
files = subprocess.check_output(cmd).split(b'\0')
|
||||
return [f.decode('utf-8') for f in files]
|
||||
|
||||
|
||||
def convert_tabs_to_spaces(files: Sequence[str]) -> None:
|
||||
for f in files:
|
||||
print("TabExpand", f)
|
||||
with open(f, 'r', encoding="utf-8") as fh:
|
||||
data = fh.read()
|
||||
# Simple 4 space (but we're using 2 spaces).
|
||||
# `data = data.expandtabs(4)`
|
||||
|
||||
# Complex 2 space
|
||||
# because some comments have tabs for alignment.
|
||||
def handle(line: str) -> str:
|
||||
line_strip = line.lstrip("\t")
|
||||
d = len(line) - len(line_strip)
|
||||
if d != 0:
|
||||
return (" " * d) + line_strip.expandtabs(4)
|
||||
return line.expandtabs(4)
|
||||
|
||||
lines = data.splitlines(keepends=True)
|
||||
lines = [handle(line) for line in lines]
|
||||
data = "".join(lines)
|
||||
with open(f, 'w', encoding="utf-8") as fh:
|
||||
fh.write(data)
|
||||
|
||||
|
||||
def clang_format_ensure_version() -> tuple[int, int, int] | None:
|
||||
global CLANG_FORMAT_CMD
|
||||
clang_format_cmd = None
|
||||
version_output = ""
|
||||
for i in range(2, -1, -1):
|
||||
clang_format_cmd = (
|
||||
"clang-format-" + (".".join(["{:d}"] * i).format(*VERSION_MIN[:i]))
|
||||
if i > 0 else
|
||||
"clang-format"
|
||||
)
|
||||
try:
|
||||
version_output = subprocess.check_output((clang_format_cmd, "-version")).decode('utf-8')
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
CLANG_FORMAT_CMD = clang_format_cmd
|
||||
break
|
||||
version: str | None = next(iter(v for v in version_output.split() if v[0].isdigit()), None)
|
||||
if version is None:
|
||||
return None
|
||||
|
||||
version = version.split("-")[0]
|
||||
# Ensure exactly 3 numbers.
|
||||
version_num: tuple[int, int, int] = (tuple(int(n) for n in version.split(".")) + (0, 0, 0))[:3] # type: ignore
|
||||
print("Using {:s} ({:d}.{:d}.{:d})...".format(CLANG_FORMAT_CMD, version_num[0], version_num[1], version_num[2]))
|
||||
return version_num
|
||||
|
||||
|
||||
def clang_format_file(files: list[str]) -> bytes:
|
||||
cmd = [
|
||||
CLANG_FORMAT_CMD,
|
||||
# Update the files in-place.
|
||||
"-i",
|
||||
# Shows the list of processed files.
|
||||
"-verbose",
|
||||
] + files
|
||||
return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def clang_print_output(output: bytes) -> None:
|
||||
print(output.decode('utf8', errors='ignore').strip())
|
||||
|
||||
|
||||
def clang_format(files: list[str]) -> None:
|
||||
pool = multiprocessing.Pool()
|
||||
|
||||
# Process in chunks to reduce overhead of starting processes.
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
chunk_size = min(max(len(files) // cpu_count // 2, 1), 32)
|
||||
for i in range(0, len(files), chunk_size):
|
||||
files_chunk = files[i:i + chunk_size]
|
||||
pool.apply_async(clang_format_file, args=[files_chunk], callback=clang_print_output)
|
||||
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Format C/C++/GLSL & Objective-C source code.",
|
||||
epilog=__doc__,
|
||||
# Don't re-wrap text, keep newlines & indentation.
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expand-tabs",
|
||||
dest="expand_tabs",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="Run a pre-pass that expands tabs "
|
||||
"(default=False)",
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--changed-only",
|
||||
dest="changed_only",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=(
|
||||
"Format only edited files, including the staged ones. "
|
||||
"Using this with \"paths\" will pick the edited files lying on those paths. "
|
||||
"(default=False)"
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="All trailing arguments are treated as paths.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
version = clang_format_ensure_version()
|
||||
if version is None:
|
||||
print("Unable to detect 'clang-format -version'")
|
||||
return 1
|
||||
if version < VERSION_MIN:
|
||||
print("Version of clang-format is too old:", version, "<", VERSION_MIN)
|
||||
return 1
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
use_default_paths = not (bool(args.paths) or bool(args.changed_only))
|
||||
paths = compute_paths(args.paths, use_default_paths)
|
||||
# Check if user-defined paths exclude all clang-format sources.
|
||||
if args.paths and not paths:
|
||||
print("Skip clang-format: no target to format")
|
||||
return 0
|
||||
|
||||
print("Operating on:" + (" ({:d} changed paths)".format(len(paths)) if args.changed_only else ""))
|
||||
for p in paths:
|
||||
print(" ", p)
|
||||
|
||||
# Notes:
|
||||
# - Paths from GIT always use forward slashes (even on WIN32),
|
||||
# so there is no need to convert slashes.
|
||||
# - Ensure a trailing slash so a `str.startswith` check can be used.
|
||||
ignore_directories_tuple = tuple(p.rstrip("/") + "/" for p in ignore_directories)
|
||||
|
||||
files = [
|
||||
f for f in source_files_from_git(paths, args.changed_only)
|
||||
if f.endswith(extensions)
|
||||
if f not in ignore_files
|
||||
if not f.startswith(ignore_directories_tuple)
|
||||
|
||||
]
|
||||
|
||||
if args.expand_tabs:
|
||||
# Always operate on all CMAKE files (when expanding tabs and no paths given).
|
||||
files_retab = [
|
||||
f for f in source_files_from_git((".",) if use_default_paths else paths, args.changed_only)
|
||||
if f.endswith(extensions_only_retab)
|
||||
if f not in ignore_files
|
||||
if not f.startswith(ignore_directories_tuple)
|
||||
]
|
||||
convert_tabs_to_spaces(files + files_retab)
|
||||
|
||||
clang_format(files)
|
||||
|
||||
if version > VERSION_MAX_RECOMMENDED:
|
||||
print()
|
||||
print(
|
||||
"WARNING: Version of clang-format is too recent:",
|
||||
version, ">", VERSION_MAX_RECOMMENDED,
|
||||
)
|
||||
print(
|
||||
"You may want to install clang-format-{:d}.{:d}, "
|
||||
"or use the precompiled libs repository.".format(
|
||||
VERSION_MAX_RECOMMENDED[0], VERSION_MAX_RECOMMENDED[1],
|
||||
),
|
||||
)
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
248
blender-5.2.0/tools/utils_maintenance/clang_tidy.py
Executable file
248
blender-5.2.0/tools/utils_maintenance/clang_tidy.py
Executable file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Tool to inspect and automatically fix clang-tidy warnings and hints.
|
||||
|
||||
Run complete checks:
|
||||
./tools/utils_maintenance/clang_tidy.py check -- source
|
||||
|
||||
Perform safe auto fixes:
|
||||
./tools/utils_maintenance/clang_tidy.py fix --config=safe -- source
|
||||
|
||||
Run checks without auto fixing:
|
||||
./tools/utils_maintenance/clang_tidy.py check --config=safe -- source
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import pathlib
|
||||
import sys
|
||||
import subprocess
|
||||
from collections.abc import (
|
||||
Sequence,
|
||||
)
|
||||
from concurrent.futures import (
|
||||
ProcessPoolExecutor,
|
||||
)
|
||||
|
||||
base_dir = pathlib.Path(__file__).parent.parent.parent.resolve()
|
||||
|
||||
extensions = (
|
||||
".c", ".cc", ".cpp", ".cxx",
|
||||
".h", ".hh", ".hpp", ".hxx",
|
||||
".m", ".mm",
|
||||
)
|
||||
|
||||
# A subset of checks that are safe and useful to fix.
|
||||
config_safe = """
|
||||
Checks: >
|
||||
-*,
|
||||
modernize-min-max-use-initializer-list,
|
||||
modernize-redundant-void-arg,
|
||||
modernize-use-bool-literals,
|
||||
modernize-use-nullptr,
|
||||
modernize-use-override,
|
||||
modernize-use-starts-ends-with,
|
||||
readability-braces-around-statements,
|
||||
readability-container-contains,
|
||||
readability-duplicate-include,
|
||||
readability-qualified-auto,
|
||||
readability-redundant-inline-specifier
|
||||
"""
|
||||
|
||||
# These sometimes seem to make mistakes, so need manual verification.
|
||||
config_unsafe = """
|
||||
Checks: >
|
||||
-*,
|
||||
modernize-deprecated-headers,
|
||||
modernize-use-equals-default,
|
||||
modernize-use-ranges,
|
||||
modernize-use-using,
|
||||
readability-redundant-casting,
|
||||
readability-use-std-min-max
|
||||
"""
|
||||
|
||||
# Config to fix automatically add const. This requires some manual fixes due to wrong
|
||||
# changes with functions pointers. And to avoid changing the existing const qualifier
|
||||
# order it can be used as follows:
|
||||
#
|
||||
# echo "QualifierAlignment: Left" >> .clang-format
|
||||
# make format
|
||||
# git commit source
|
||||
#
|
||||
# ./tools/utils_maintenance/clang_tidy.py fix --config=const -- source
|
||||
# make format
|
||||
# git commit source
|
||||
#
|
||||
# git checkout .clang-format
|
||||
# git revert HEAD~1
|
||||
#
|
||||
config_const = """
|
||||
Checks: >
|
||||
-*,
|
||||
misc-const-correctness,
|
||||
readability-non-const-parameter,
|
||||
|
||||
CheckOptions:
|
||||
- key: misc-const-correctness.WarnPointersAsPointers
|
||||
value: 1
|
||||
- key: misc-const-correctness.TransformPointersAsPointers
|
||||
value: 1
|
||||
"""
|
||||
|
||||
# Config to remove unused includes.
|
||||
# This can easily break with different platforms and build options, and also
|
||||
# requires LLVM version 23 or newer to support disabling MissingIncludes.
|
||||
config_includes = """
|
||||
Checks: >
|
||||
-*,
|
||||
misc-include-cleaner
|
||||
|
||||
CheckOptions:
|
||||
- key: misc-include-cleaner.MissingIncludes
|
||||
value: 0
|
||||
- key: misc-include-cleaner.UnusedIncludes
|
||||
value: 1
|
||||
"""
|
||||
|
||||
configs = {
|
||||
"complete": None,
|
||||
"safe": config_safe,
|
||||
"unsafe": config_unsafe,
|
||||
"const": config_const,
|
||||
"includes": config_includes,
|
||||
}
|
||||
|
||||
|
||||
def source_files_from_git(paths: Sequence[str]) -> list[str]:
|
||||
cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z")
|
||||
try:
|
||||
files_bytes = subprocess.check_output(cmd, cwd=base_dir).split(b"\0")
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
files = [f.decode("utf-8") for f in files_bytes if f]
|
||||
return [f for f in files if f.endswith(extensions)]
|
||||
|
||||
|
||||
def process_file(
|
||||
file_path: str,
|
||||
tidy_config: str | None,
|
||||
fix: bool,
|
||||
compile_commands_dir: str,
|
||||
done: multiprocessing.managers.ValueProxy[int],
|
||||
lock: contextlib.AbstractContextManager[bool],
|
||||
total: int,
|
||||
) -> None:
|
||||
# Progress display.
|
||||
with lock:
|
||||
done.value += 1
|
||||
progress_text = f"[{done.value}/{total}] {file_path}"
|
||||
if sys.stdout.isatty():
|
||||
sys.stdout.write(f"\r{progress_text}\033[K")
|
||||
else:
|
||||
sys.stdout.write(f"{progress_text}\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
cmd = ["clang-tidy", "-p", compile_commands_dir]
|
||||
if tidy_config:
|
||||
cmd.append(f"--config={tidy_config}")
|
||||
if fix:
|
||||
cmd.append("--fix")
|
||||
cmd.append("--quiet")
|
||||
cmd.append(file_path)
|
||||
|
||||
# Ignore errors as clang-tidy returns non-zero for irrelevant errors.
|
||||
if fix:
|
||||
subprocess.run(
|
||||
cmd,
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=base_dir,
|
||||
)
|
||||
else:
|
||||
subprocess.run(cmd, check=False, cwd=base_dir)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run clang-tidy on files.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
# Show full help instead of short usage on argument errors.
|
||||
parser.print_usage = parser.print_help # type: ignore[method-assign]
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=["check", "fix"],
|
||||
help="Check or auto-fix",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
choices=configs.keys(),
|
||||
default="complete",
|
||||
help="Config to use, see the description for details",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--compile-commands-dir",
|
||||
default=str(base_dir),
|
||||
help="Directory containing compile_commands.json (default: project root)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="+",
|
||||
help="Files or directories to process.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
compile_commands = pathlib.Path(args.compile_commands_dir) / "compile_commands.json"
|
||||
if not compile_commands.exists():
|
||||
sys.stderr.write(f"Error: {compile_commands} not found\n")
|
||||
sys.stderr.write("Enable CMAKE_EXPORT_COMPILE_COMMANDS or use --compile-commands-dir=/build/dir/\n")
|
||||
sys.exit(1)
|
||||
|
||||
if args.command == "fix" and args.config == "complete":
|
||||
parser.error("The 'complete' configuration cannot be used with the 'fix' command.")
|
||||
|
||||
files = source_files_from_git(args.paths)
|
||||
if not files:
|
||||
print("No files found to process.")
|
||||
return
|
||||
|
||||
jobs = multiprocessing.cpu_count()
|
||||
total_files = len(files)
|
||||
|
||||
with multiprocessing.Manager() as mpm:
|
||||
done = mpm.Value("i", 0)
|
||||
lock = mpm.Lock()
|
||||
|
||||
with ProcessPoolExecutor(max_workers=jobs) as executor:
|
||||
futures = [
|
||||
executor.submit(
|
||||
process_file,
|
||||
f,
|
||||
configs[args.config],
|
||||
args.command == "fix",
|
||||
args.compile_commands_dir,
|
||||
done,
|
||||
lock,
|
||||
total_files,
|
||||
)
|
||||
for f in files
|
||||
]
|
||||
for future in futures:
|
||||
future.result()
|
||||
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
167
blender-5.2.0/tools/utils_maintenance/cmake_sort_filelists.py
Executable file
167
blender-5.2.0/tools/utils_maintenance/cmake_sort_filelists.py
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Sorts CMake path lists
|
||||
- Don't cross blank newline boundaries.
|
||||
- Don't cross different path prefix boundaries.
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
PWD = os.path.dirname(__file__)
|
||||
sys.path.append(os.path.join(PWD, "modules"))
|
||||
|
||||
from batch_edit_text import run
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(PWD, "..", ".."))))
|
||||
|
||||
# TODO, move to config file
|
||||
SOURCE_DIRS = (
|
||||
"source",
|
||||
"intern/ghost",
|
||||
)
|
||||
|
||||
SOURCE_EXT = (
|
||||
# C/C++
|
||||
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
|
||||
# Objective C
|
||||
".m", ".mm",
|
||||
)
|
||||
|
||||
# CMake commands where argument order may be significant (e.g. positional arguments),
|
||||
# all lines within these commands are excluded from sorting.
|
||||
SORT_SKIP_COMMANDS: set[str] = {
|
||||
"add_custom_command",
|
||||
}
|
||||
|
||||
# CMake variable names where the order of entries may be significant,
|
||||
# all lines within `set(<name> ...)` or `list(APPEND <name> ...)` are excluded from sorting.
|
||||
SORT_SKIP_VARIABLES: set[str] = {
|
||||
"LIB",
|
||||
"TEST_LIB",
|
||||
}
|
||||
|
||||
|
||||
def sort_cmake_file_lists(fn: str, data_src: str) -> str | None:
|
||||
fn_dir = os.path.dirname(fn)
|
||||
lines = data_src.splitlines(keepends=True)
|
||||
|
||||
def can_sort(l: str) -> bool:
|
||||
l = l.split("#", 1)[0].strip()
|
||||
# Source files.
|
||||
if l.endswith(SOURCE_EXT):
|
||||
if "(" not in l and ')' not in l:
|
||||
return True
|
||||
# Headers.
|
||||
if l and os.path.isdir(os.path.join(fn_dir, l)):
|
||||
return True
|
||||
# Libraries.
|
||||
if l.startswith(("bf_", "extern_")) and "." not in l and "/" not in l:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_sort_compat(a: str, b: str) -> bool:
|
||||
# Strip comments.
|
||||
a = a.split("#", 1)[0]
|
||||
b = b.split("#", 1)[0]
|
||||
|
||||
# Compare leading white-space.
|
||||
if a[:-(len(a.lstrip()))] == b[:-(len(b.lstrip()))]:
|
||||
# return False
|
||||
|
||||
# Compare loading paths.
|
||||
a_ls = a.split("/")
|
||||
b_ls = b.split("/")
|
||||
if len(a_ls) == 1 and len(b_ls) == 1:
|
||||
return True
|
||||
if len(a_ls) == len(b_ls):
|
||||
if len(a_ls) == 1:
|
||||
return True
|
||||
if a_ls[:-1] == b_ls[:-1]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def calc_skip_lines(lines: list[str]) -> set[int]:
|
||||
"""
|
||||
Compute line indices to skip sorting, where order may be significant
|
||||
(e.g. positional command arguments or library link order).
|
||||
"""
|
||||
|
||||
def is_skip_command(cmd_name: str, args: list[str]) -> bool:
|
||||
if cmd_name in SORT_SKIP_COMMANDS:
|
||||
return True
|
||||
if cmd_name == "set":
|
||||
if args and args[0] in SORT_SKIP_VARIABLES:
|
||||
return True
|
||||
if cmd_name == "list":
|
||||
if len(args) >= 2 and args[1] in SORT_SKIP_VARIABLES:
|
||||
return True
|
||||
return False
|
||||
|
||||
skip_lines: set[int] = set()
|
||||
skip_depth = 0
|
||||
in_skip_cmd = False
|
||||
for i, line in enumerate(lines):
|
||||
line_strip = line.split("#", 1)[0]
|
||||
if not in_skip_cmd:
|
||||
ls = line_strip.lstrip()
|
||||
paren_pos = ls.find("(")
|
||||
if paren_pos != -1:
|
||||
cmd_name = ls[:paren_pos].strip()
|
||||
args = ls[paren_pos + 1:].split()
|
||||
if is_skip_command(cmd_name, args):
|
||||
in_skip_cmd = True
|
||||
skip_depth = 0
|
||||
if in_skip_cmd:
|
||||
skip_lines.add(i)
|
||||
skip_depth += line_strip.count("(") - line_strip.count(")")
|
||||
if skip_depth <= 0:
|
||||
in_skip_cmd = False
|
||||
return skip_lines
|
||||
|
||||
skip_lines = calc_skip_lines(lines)
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if can_sort(lines[i]):
|
||||
j = i
|
||||
while j + 1 < len(lines):
|
||||
if not can_sort(lines[j + 1]):
|
||||
break
|
||||
if not can_sort_compat(lines[i], lines[j + 1]):
|
||||
break
|
||||
j = j + 1
|
||||
if i != j:
|
||||
# Skip blocks span full commands; their boundaries contain
|
||||
# parentheses which fail `can_sort`, so a sortable group
|
||||
# cannot straddle a skip boundary - checking `i` suffices.
|
||||
if i not in skip_lines:
|
||||
lines[i:j + 1] = list(sorted(lines[i:j + 1]))
|
||||
i = j
|
||||
i = i + 1
|
||||
|
||||
data_dst = "".join(lines)
|
||||
if data_src != data_dst:
|
||||
return data_dst
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
run(
|
||||
directories=[os.path.join(SOURCE_DIR, d) for d in SOURCE_DIRS],
|
||||
is_text=lambda fn: fn.endswith("CMakeLists.txt"),
|
||||
text_operation=sort_cmake_file_lists,
|
||||
use_multiprocess=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
2328
blender-5.2.0/tools/utils_maintenance/code_clean.py
Executable file
2328
blender-5.2.0/tools/utils_maintenance/code_clean.py
Executable file
File diff suppressed because it is too large
Load Diff
634
blender-5.2.0/tools/utils_maintenance/make_license.py
Normal file
634
blender-5.2.0/tools/utils_maintenance/make_license.py
Normal file
@@ -0,0 +1,634 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from typing import (
|
||||
NamedTuple,
|
||||
)
|
||||
from collections.abc import (
|
||||
Iterator,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Path Constants
|
||||
|
||||
ROOT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
DIRPATH_LICENSES: Path = ROOT_DIR / "release/license/"
|
||||
DIRPATH_EXTERN_LIBRARIES: Path = ROOT_DIR / "extern"
|
||||
|
||||
FILEPATH_VERSIONS_CMAKE: Path = ROOT_DIR / "build_files/build_environment/cmake/versions.cmake"
|
||||
FILEPATH_LICENSES_INDEX: Path = DIRPATH_LICENSES / "licenses.json" # List of licenses and definitions.
|
||||
FILEPATH_LICENSE_GENERATED: Path = DIRPATH_LICENSES / "license.md" # Generated licenses file.
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
INTRODUCTION = r"""<!--
|
||||
|
||||
This document is auto-generated with `make license`.
|
||||
To update it, edit (paths relative to Blender projects root):
|
||||
|
||||
* Introduction and formatting: ./tools/utils_maintenance/make_license.py
|
||||
* External libraries: ./build_files/build_environment/cmake/versions.cmake
|
||||
* Internal libraries: ./extern/*/Blender.README
|
||||
* Fonts: ./tools/utils_maintenance/make_license.py
|
||||
* New licenses: ./release/license/licenses.json
|
||||
|
||||
Then run `make license` and commit `license.md`.
|
||||
|
||||
-->
|
||||
# Blender Third-Party Licenses
|
||||
|
||||
While Blender itself is released under [GPU-GPL 3.0 or later](https://spdx.org/licenses/GPL-3.0-or-later.html)
|
||||
`© 2011-<THIS-YEAR> Blender Foundation`,
|
||||
it contains dependencies which have different licenses.
|
||||
|
||||
<SPDX:GPL-3.0-or-later>
|
||||
|
||||
""".replace("<THIS-YEAR>", str(datetime.date.today().year))
|
||||
|
||||
INTRODUCTION += r"""
|
||||
## Fonts
|
||||
|
||||
Blender distributes a number of font files to support many different language and uses.
|
||||
They work together as a stack.
|
||||
|
||||
| Font | License | Copyright |
|
||||
| ------- | --------- | ------- |
|
||||
| [Inter](https://rsms.me/inter/) | <SPDX:OFL-1.1|link> | `Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)` |
|
||||
| [Noto Fonts](https://fonts.google.com/noto) | <SPDX:OFL-1.1|link> | `Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)`|
|
||||
| [Last Resort](https://github.com/unicode-org/last-resort-font) | <SPDX:OFL-1.1|link> | `Copyright © 1998-2024 Unicode, Inc. Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the United States and other countries.` |
|
||||
| [DejaVu Sans Mono](https://github.com/dejavu-fonts/dejavu-fonts) | <Arev-Fonts|link> + <SPDX:Bitstream-Vera|link> | `2003 Bitstream, Inc. (Bitstream font glyphs). 2006 Tavmjong Bah (Arev font glyphs). DejaVu changes are in public domain` |
|
||||
|
||||
<Arev-Fonts>
|
||||
<SPDX:Bitstream-Vera>
|
||||
<SPDX:OFL-1.1>
|
||||
"""
|
||||
|
||||
|
||||
NO_LICENSE = "No License Set"
|
||||
|
||||
LICENSES_NOT_NEEDED = {
|
||||
"",
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Types
|
||||
|
||||
|
||||
# Raw data extracted either from:
|
||||
# - `README.blender` files.
|
||||
# - `./build_files/build_environment/cmake/versions.cmake`.
|
||||
class LibraryRaw(NamedTuple):
|
||||
name: str
|
||||
homepage: str
|
||||
version: str
|
||||
license: str
|
||||
exception: str
|
||||
copyright: str
|
||||
|
||||
|
||||
def int_to_superscript(num: int) -> str:
|
||||
# Mapping of regular digits to superscript Unicode characters.
|
||||
superscript_map = {
|
||||
"0": "⁰",
|
||||
"1": "¹",
|
||||
"2": "²",
|
||||
"3": "³",
|
||||
"4": "⁴",
|
||||
"5": "⁵",
|
||||
"6": "⁶",
|
||||
"7": "⁷",
|
||||
"8": "⁸",
|
||||
"9": "⁹"
|
||||
}
|
||||
# Convert the integer to a string and map each digit to its superscript equivalent.
|
||||
return "".join(superscript_map[digit] for digit in str(num))
|
||||
|
||||
|
||||
class Library:
|
||||
__slots__ = ("name", "version", "homepage", "library_copyright", "exception")
|
||||
|
||||
name: str
|
||||
version: str
|
||||
homepage: str
|
||||
library_copyright: str
|
||||
exception: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
version: str,
|
||||
homepage: str,
|
||||
library_copyright: str,
|
||||
exception: str,
|
||||
):
|
||||
# pylint: disable=too-many-arguments
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.homepage = homepage
|
||||
self.library_copyright = library_copyright
|
||||
self.exception = exception
|
||||
|
||||
def __lt__(self, other: "Library") -> bool:
|
||||
return self.name.lower() < other.name.lower()
|
||||
|
||||
def check_missing_copyright(self, library_license: "License") -> None:
|
||||
"""Some licenses require a copyright notice"""
|
||||
if self.library_copyright or library_license.copyright_exemption:
|
||||
return
|
||||
|
||||
print(f"Warning: \"{self.name}\" missing copyright notice "
|
||||
f"(required for {library_license.identifier}).")
|
||||
|
||||
def dump(self, library_license: "License") -> str:
|
||||
self.check_missing_copyright(library_license)
|
||||
|
||||
library_copyright = f"`{self.library_copyright}`" if self.library_copyright else "-"
|
||||
name = f"[{self.name}]({self.homepage})" if self.homepage else self.name
|
||||
version = self.version[:11] if self.version else "-"
|
||||
|
||||
# Add exception indicator in the name.
|
||||
name += library_license.get_exception_suffix(self.exception)
|
||||
raw_data = (
|
||||
f"| {name} "
|
||||
f"| {version} "
|
||||
)
|
||||
|
||||
if not library_license.copyright_exemption:
|
||||
raw_data += f"| {library_copyright} "
|
||||
|
||||
raw_data += "|\n"
|
||||
return raw_data
|
||||
|
||||
|
||||
class License:
|
||||
__slots__ = ("identifier", "name", "url", "copyright_exemption", "libraries", "exceptions")
|
||||
|
||||
identifier: str
|
||||
name: str
|
||||
url: str
|
||||
copyright_exemption: str
|
||||
libraries: list[Library]
|
||||
exceptions: list[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
identifier: str,
|
||||
name: str,
|
||||
url: str,
|
||||
copyright_exemption: str = "",
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.name = name
|
||||
self.url = url
|
||||
# By default we assume that all the licenses require copyright.
|
||||
self.copyright_exemption = copyright_exemption
|
||||
self.libraries = []
|
||||
self.exceptions = []
|
||||
|
||||
@property
|
||||
def filepath(self) -> str:
|
||||
if self.identifier.startswith("SPDX"):
|
||||
filepath = os.path.join(DIRPATH_LICENSES, "spdx", f"{self.identifier[5:]}.txt")
|
||||
else:
|
||||
filepath = os.path.join(DIRPATH_LICENSES, "others", f"{self.identifier}.txt")
|
||||
return filepath
|
||||
|
||||
def get_exception_suffix(self, exception: str) -> str:
|
||||
"""Return (optional) exception indicator e.g., ¹ """
|
||||
if not exception:
|
||||
return ""
|
||||
|
||||
if exception not in self.exceptions:
|
||||
self.exceptions.append(exception)
|
||||
|
||||
_id = self.exceptions.index(exception)
|
||||
return int_to_superscript(_id + 1)
|
||||
|
||||
def dump(self, index: int = 0) -> str:
|
||||
"""Read the complete license file from disk and return as string
|
||||
"""
|
||||
# Make sure we only throw the error if we actually need the file.
|
||||
# If there are no libraries using this license, there is no need to complain.
|
||||
# The json could even have all the licenses from SPDX and only include the ones
|
||||
# Blender needs.
|
||||
if not os.path.exists(self.filepath):
|
||||
if self.copyright_exemption:
|
||||
return ""
|
||||
|
||||
print(f"Error: Could not find license file for {self.identifier}: \"{self.filepath}\"")
|
||||
sys.exit(1)
|
||||
|
||||
with open(self.filepath, "r", encoding="utf8") as fh:
|
||||
license_raw = fh.read()
|
||||
|
||||
# Strip trailing space as this has a special meaning for mark-down,
|
||||
# avoid editing the original texts as any edits may be overwritten
|
||||
# when updating the licenses.
|
||||
#
|
||||
# This also removes page breaks "\x0C" or ^L.
|
||||
# These could be replaced with sometime similar in markdown,
|
||||
# unless this has some benefit, leave as-is.
|
||||
license_raw = "\n".join(line.rstrip() for line in license_raw.split("\n"))
|
||||
|
||||
# Debug option commented out.
|
||||
# This is useful if you want a document for human inspection without the licenses.
|
||||
# license_raw = "# Debug"
|
||||
|
||||
summary_prefix = f"{int_to_superscript(index)} " if index else ""
|
||||
library_license = (
|
||||
f"<details>\n<summary>{summary_prefix}{self.name}</summary>\n"
|
||||
f"\n{license_raw}\n"
|
||||
"</details>"
|
||||
)
|
||||
|
||||
return library_license
|
||||
|
||||
def __lt__(self, other: "License") -> bool:
|
||||
return self.name.lower() < other.name.lower()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
as_dict = {
|
||||
self.identifier: {
|
||||
"name": self.name,
|
||||
"url": self.url,
|
||||
"filepath": self.filepath,
|
||||
"libraries": len(self.libraries),
|
||||
}
|
||||
}
|
||||
return json.dumps(as_dict, indent=2)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Internal Logic
|
||||
|
||||
def initialize_licenses() -> dict[str, License]:
|
||||
with open(FILEPATH_LICENSES_INDEX, "r", encoding="utf8") as fh:
|
||||
licenses_json = json.load(fh)
|
||||
|
||||
licenses = {key: License(identifier=key, **values) for key, values in licenses_json.items()}
|
||||
return licenses
|
||||
|
||||
|
||||
def get_license_exception(library_license: str) -> tuple[str, str]:
|
||||
"""Split license into main license and exception
|
||||
|
||||
Example of acceptable license: "SPDX:Apache-2.0 WITH LLVM-exception"
|
||||
This would output: ("SPDX:Apache-2.0", "LLVM-exception")
|
||||
"""
|
||||
# Use `re.IGNORECASE` to match "with" in any case (e.g., "with" or "WITH").
|
||||
re_match = re.match(r"^(.*)\swith\s(.+)$", library_license, re.IGNORECASE)
|
||||
if re_match:
|
||||
return re_match.group(1).strip(), re_match.group(2).strip()
|
||||
return library_license, ""
|
||||
|
||||
|
||||
def flatten_cmake_file(content: str) -> str:
|
||||
"""Resolve all the ${VARIABLES} in CMake"""
|
||||
# Find all variable definitions of the form `set(VAR_NAME VALUE)`.
|
||||
variables = dict(re.findall(r"set\((\w+)\s+([^\)]+)\)", content))
|
||||
|
||||
# Replace all occurrences of ${VAR_NAME} with the corresponding value.
|
||||
for var, value in variables.items():
|
||||
content = re.sub(rf"\$\{{{var}\}}", value, content)
|
||||
return content
|
||||
|
||||
|
||||
def process_versions_cmake() -> Iterator[LibraryRaw]:
|
||||
"""
|
||||
Parse versions.cmake
|
||||
|
||||
Return a dictionary grouped by license.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
# NOTE(@ideasman42): basic & imperfect variable extractions.
|
||||
# It can be fairly easily tripped up by expressions such as:
|
||||
# - `set(VAR "VALUE ) # ")`
|
||||
# - Uppercase or additional spaces e.g. `SET (...)`.
|
||||
# - Or `set()` expressions inside a multi-line string.
|
||||
#
|
||||
# Any effort to rewrite this logic would be better spent running the file through CMake it's self,
|
||||
# appending logic to dump all variables using `string(JSON ...)` which Python can then read reliably.
|
||||
|
||||
libraries_raw: dict[str, dict[str, str]] = {}
|
||||
with open(FILEPATH_VERSIONS_CMAKE, "r", encoding="utf8") as fh:
|
||||
data = fh.read()
|
||||
|
||||
data = flatten_cmake_file(data)
|
||||
for re_match in re.finditer(r"^set\((\w+)\s+", data, re.MULTILINE):
|
||||
# Use regex to capture the key from each set() statement.
|
||||
# Extract the value from the remainder.
|
||||
key = re_match.group(1)
|
||||
value_start = re_match.end()
|
||||
value_eol = data.find("\n", value_start)
|
||||
assert value_eol != -1
|
||||
value_line = data[value_start: value_eol].rstrip()
|
||||
|
||||
# Strip any comments at the line end.
|
||||
# `set(FOO BAR) # BAZ`.
|
||||
if (re_match_comment := next(re.finditer(r"\)\s*#", value_line), None)):
|
||||
value_line = value_line[:re_match_comment.start() + 1]
|
||||
|
||||
# Extract the value by checking this line and detecting single or multi-line text.
|
||||
if value_line.endswith(")"):
|
||||
# Single line variable.
|
||||
value = value_line[:-1].strip()
|
||||
elif value_line.endswith("[=["):
|
||||
# Calculate the bounds of the multi-line string.
|
||||
value_ml_start = value_start + len(value_line)
|
||||
value_ml_end = data.find("]=]", value_ml_start)
|
||||
assert value_ml_end != -1
|
||||
value = data[value_ml_start:value_ml_end].strip().replace("\n", " ")
|
||||
else:
|
||||
# Could not detect a single line value OR a multi-line value.
|
||||
print(f"Error: Unable to parse {key!r}, line {value_line!r}, "
|
||||
"expected an \")\" ending or beginning of a multi-line string \"[=[\"")
|
||||
sys.exit(1)
|
||||
|
||||
# Determine the library name from the prefix (minus the suffix).
|
||||
library_name, end_word = key.rpartition("_")[0::2]
|
||||
if not library_name:
|
||||
# No suffix to check, it can be skipped.
|
||||
continue
|
||||
|
||||
# Initialize the library entry if it doesn't exist.
|
||||
if (library_vars := libraries_raw.get(library_name)) is None:
|
||||
library_vars = libraries_raw[library_name] = {
|
||||
"name": library_name.replace("_", " ").title(),
|
||||
"homepage": "",
|
||||
"version": "",
|
||||
"license": "",
|
||||
"exception": "",
|
||||
"copyright": "",
|
||||
# Exclude from `LibraryRaw`.
|
||||
"_hash": "",
|
||||
"_build_time_only": "",
|
||||
}
|
||||
|
||||
# Populate the relevant fields based on the key.
|
||||
match end_word:
|
||||
case "NAME":
|
||||
library_vars["name"] = value.strip('"')
|
||||
case "HOMEPAGE":
|
||||
library_vars["homepage"] = value.strip('"')
|
||||
case "VERSION":
|
||||
library_vars["version"] = value.strip('"')
|
||||
case "LICENSE":
|
||||
library_license, exception = get_license_exception(value)
|
||||
library_vars["license"] = library_license
|
||||
library_vars["exception"] = exception
|
||||
case "COPYRIGHT":
|
||||
library_vars["copyright"] = value.strip('"')
|
||||
case "HASH":
|
||||
library_vars["_hash"] = value
|
||||
case "DEPSBUILDTIMEONLY":
|
||||
# Use only strings to simplify the type-checking.
|
||||
library_vars["_build_time_only"] = "True"
|
||||
|
||||
# If there is no hash we assume it is not a real library but some other information on the file.
|
||||
# Also remove any library which is only used during build time and have no
|
||||
# artifact included in the final Blender binary.
|
||||
for key, lib_info_args in libraries_raw.items():
|
||||
if not (lib_info_args["_hash"] and not lib_info_args["_build_time_only"]):
|
||||
continue
|
||||
yield LibraryRaw(**{k: v for k, v in lib_info_args.items() if not k.startswith("_")})
|
||||
|
||||
|
||||
def iterate_readme_files(base_dir: Path) -> Iterator[str]:
|
||||
base_path = Path(base_dir)
|
||||
|
||||
# Iterate over all subdirectories.
|
||||
for subdir in base_path.iterdir():
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
|
||||
readme_path = subdir / "README.blender"
|
||||
if not readme_path.exists():
|
||||
print(f"Warning: Missing file \"{readme_path}\"")
|
||||
continue
|
||||
|
||||
with readme_path.open("r", encoding="utf8") as fh:
|
||||
contents = fh.read()
|
||||
yield contents
|
||||
|
||||
|
||||
def process_readme_blender() -> Iterator[LibraryRaw]:
|
||||
""""Handle the README.blender files"""
|
||||
keys = {
|
||||
"Project": "name",
|
||||
"URL": "homepage",
|
||||
"License": "license",
|
||||
"Upstream version": "version",
|
||||
"Copyright": "copyright"
|
||||
}
|
||||
|
||||
for readme in iterate_readme_files(DIRPATH_EXTERN_LIBRARIES):
|
||||
lines = readme.strip().split("\n")
|
||||
|
||||
# Temporary storage for project fields.
|
||||
project_fields = {}
|
||||
|
||||
for line in lines:
|
||||
line_split = line.split(":", 1)
|
||||
|
||||
# Ignore comments and empty lines.
|
||||
if len(line_split) != 2:
|
||||
continue
|
||||
|
||||
key, value = line_split
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"')
|
||||
|
||||
# Check if the current line matches one of the provided keys.
|
||||
if key in keys:
|
||||
project_fields[keys[key]] = value
|
||||
|
||||
# Assign the fields to the project name.
|
||||
project_name = project_fields.get("name", "Unknown Project")
|
||||
|
||||
# Split the license into license and its (optional) extension.
|
||||
library_license, exception = get_license_exception(project_fields.get("license", ""))
|
||||
|
||||
yield LibraryRaw(
|
||||
name=project_name,
|
||||
version=project_fields.get("version", ""),
|
||||
homepage=project_fields.get("homepage", ""),
|
||||
license=library_license,
|
||||
exception=exception,
|
||||
copyright=project_fields.get("copyright", ""),
|
||||
)
|
||||
|
||||
|
||||
def fetch_libraries_licenses() -> dict[str, License]:
|
||||
"""Populate the licenses dict with its corresponding libraries and copyrights"""
|
||||
|
||||
licenses = initialize_licenses()
|
||||
|
||||
# Intermediate storage.
|
||||
|
||||
# Map the license name to all libraries that use it.
|
||||
# Keys may be: `SPDX:GPL-2.0-or-later`, `SPDX:MIT`, ... `ICS` etc.
|
||||
licenses_data: dict[str, list[LibraryRaw]] = {}
|
||||
|
||||
for lib_info in itertools.chain(
|
||||
# Get data from `./build_files/build_environment/cmake/versions.cmake`.
|
||||
process_versions_cmake(),
|
||||
# Get data from `README.blender` files.
|
||||
process_readme_blender(),
|
||||
):
|
||||
license_name = lib_info.license or NO_LICENSE
|
||||
if (libraries_data := licenses_data.get(license_name)) is None:
|
||||
libraries_data = licenses_data[license_name] = []
|
||||
libraries_data.append(lib_info)
|
||||
|
||||
# Populate licenses with the corresponding libraries.
|
||||
for license_key, libraries_data in licenses_data.items():
|
||||
if license_key == NO_LICENSE:
|
||||
print("Warning: The following libraries have no license:")
|
||||
for lib_info in libraries_data:
|
||||
print(f" * {lib_info.name}")
|
||||
continue
|
||||
if license_key in LICENSES_NOT_NEEDED:
|
||||
# Do nothing about these licenses.
|
||||
continue
|
||||
if (license_obj := licenses.get(license_key)) is None:
|
||||
# Do nothing about these licenses.
|
||||
print(f"Error: {license_key} license not found in: \"{FILEPATH_LICENSES_INDEX}\"")
|
||||
continue
|
||||
|
||||
for lib_info in libraries_data:
|
||||
library = Library(
|
||||
name=lib_info.name,
|
||||
version=lib_info.version,
|
||||
homepage=lib_info.homepage,
|
||||
library_copyright=lib_info.copyright,
|
||||
exception=lib_info.exception,
|
||||
)
|
||||
license_obj.libraries.append(library)
|
||||
|
||||
return licenses
|
||||
|
||||
|
||||
def extract_licenses(text: str) -> set[str]:
|
||||
"""Extract all the licenses from the text
|
||||
|
||||
Licenses are defined under <>, and |link is ignored.
|
||||
|
||||
For example, for the input:
|
||||
* <SPDX:GPL-3.0-or-later|link>
|
||||
* <Example-Fonts>
|
||||
|
||||
The output would be:
|
||||
{"SPDX:GPL-3.0-or-later", "Example-Fonts"}
|
||||
"""
|
||||
# Remove multi-line comments (<!-- ... -->).
|
||||
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
||||
|
||||
# Find all licenses in < >.
|
||||
license_pattern = r"<([^<|>]+?)>"
|
||||
|
||||
# Find all matches.
|
||||
matches = re.findall(license_pattern, text)
|
||||
|
||||
# Extract unique licenses while ignoring emails.
|
||||
licenses = {match.strip() for match in matches if "@" not in match}
|
||||
|
||||
return licenses
|
||||
|
||||
|
||||
def get_introduction(licenses: dict[str, License]) -> str:
|
||||
introduction = INTRODUCTION
|
||||
license_lookups = extract_licenses(INTRODUCTION)
|
||||
|
||||
for license_lookup in license_lookups:
|
||||
if license_lookup not in licenses:
|
||||
print(f"Error: {license_lookup} license not found in: \"{FILEPATH_LICENSES_INDEX}\"")
|
||||
continue
|
||||
|
||||
license_item = licenses[license_lookup]
|
||||
introduction = introduction.replace(
|
||||
f"<{license_lookup}>",
|
||||
license_item.dump()
|
||||
)
|
||||
|
||||
introduction = introduction.replace(
|
||||
f"<{license_lookup}|link>",
|
||||
f"[{license_item.name}]({license_item.url})"
|
||||
)
|
||||
|
||||
return introduction
|
||||
|
||||
|
||||
def generate_license_file(licenses: dict[str, License]) -> None:
|
||||
filepath = FILEPATH_LICENSE_GENERATED
|
||||
|
||||
with open(filepath, "w", encoding="utf8") as fh:
|
||||
fh.write(get_introduction(licenses))
|
||||
|
||||
for license_item in sorted(licenses.values()):
|
||||
if len(license_item.libraries) == 0:
|
||||
continue
|
||||
|
||||
if license_item.url:
|
||||
fh.write(f"\n\n## [{license_item.name}]({license_item.url})\n\n")
|
||||
else:
|
||||
fh.write(f"\n\n## {license_item.name}\n\n")
|
||||
|
||||
if license_item.copyright_exemption:
|
||||
fh.write(f"{license_item.copyright_exemption}\n\n")
|
||||
fh.write("| Library | Version |\n")
|
||||
fh.write("| ------- | ------- |\n")
|
||||
else:
|
||||
fh.write("| Library | Version | Copyright |\n")
|
||||
fh.write("| ------- | ------- | --------- |\n")
|
||||
|
||||
for library in sorted(license_item.libraries):
|
||||
fh.write(library.dump(license_item))
|
||||
|
||||
fh.write(license_item.dump())
|
||||
for i, exception in enumerate(license_item.exceptions):
|
||||
exception_license = licenses.get(exception)
|
||||
|
||||
if exception_license is None:
|
||||
print(f"Error: {exception} extension license not found in: \"{FILEPATH_LICENSES_INDEX}\"")
|
||||
continue
|
||||
|
||||
fh.write(exception_license.dump(i + 1))
|
||||
fh.write("\n")
|
||||
|
||||
print(f"\nLicense file successfully generated: \"{filepath}\"")
|
||||
print("Remember to commit the file to the Blender repository.\n")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main Function
|
||||
|
||||
|
||||
def main() -> None:
|
||||
licenses = fetch_libraries_licenses()
|
||||
|
||||
generate_license_file(licenses)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"run",
|
||||
)
|
||||
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
TextOpFn = Callable[
|
||||
# file_name, data_src
|
||||
[str, str],
|
||||
# data_dst or None when no change is made.
|
||||
str | None,
|
||||
]
|
||||
|
||||
|
||||
def operation_wrap(fn: str, text_operation: TextOpFn) -> None:
|
||||
with open(fn, "r", encoding="utf-8") as f:
|
||||
data_src = f.read()
|
||||
data_dst = text_operation(fn, data_src)
|
||||
|
||||
if data_dst is None or (data_src == data_dst):
|
||||
return
|
||||
|
||||
with open(fn, "w", encoding="utf-8") as f:
|
||||
f.write(data_dst)
|
||||
|
||||
|
||||
def run(
|
||||
*,
|
||||
directories: Sequence[str],
|
||||
is_text: Callable[[str], bool],
|
||||
text_operation: TextOpFn,
|
||||
use_multiprocess: bool,
|
||||
) -> None:
|
||||
import os
|
||||
|
||||
def source_files(path: str) -> Iterator[str]:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
||||
for filename in filenames:
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
if is_text(filepath):
|
||||
yield filepath
|
||||
|
||||
if use_multiprocess:
|
||||
args = [
|
||||
(fn, text_operation) for directory in directories
|
||||
for fn in source_files(directory)
|
||||
]
|
||||
import multiprocessing
|
||||
job_total = multiprocessing.cpu_count()
|
||||
pool = multiprocessing.Pool(processes=job_total)
|
||||
pool.starmap(operation_wrap, args)
|
||||
else:
|
||||
for directory in directories:
|
||||
for fn in source_files(directory):
|
||||
operation_wrap(fn, text_operation)
|
||||
@@ -0,0 +1,88 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
When writing text checking utilities, it's not always straightforward
|
||||
to find line numbers and ranges from an offset within the text.
|
||||
|
||||
This module provides helpers to efficiently do this.
|
||||
|
||||
The main utility is ``finditer_with_line_numbers_and_bounds``,
|
||||
an alternative to ``re.finditer`` which yields line numbers and offsets
|
||||
for the line bounds - useful for scanning files and reporting errors that include the line contents.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"finditer_newline_cache_compute",
|
||||
"finditer_with_line_numbers_and_bounds",
|
||||
"line_to_offset_range",
|
||||
)
|
||||
|
||||
from collections.abc import (
|
||||
Iterator,
|
||||
)
|
||||
|
||||
import re as _re
|
||||
|
||||
|
||||
def finditer_newline_cache_compute(text: str) -> tuple[dict[int, int], list[int]]:
|
||||
"""
|
||||
Return a tuple containing:
|
||||
Offset to
|
||||
"""
|
||||
# Offset to line lookup.
|
||||
offset_to_line_cache: dict[int, int] = {}
|
||||
# Line to offset lookup.
|
||||
line_to_offset_cache: list[int] = [0]
|
||||
|
||||
for i, m in enumerate(_re.finditer("\\n", text), 1):
|
||||
ofs = m.start()
|
||||
offset_to_line_cache[ofs] = i
|
||||
line_to_offset_cache.append(ofs)
|
||||
|
||||
return offset_to_line_cache, line_to_offset_cache
|
||||
|
||||
|
||||
def finditer_with_line_numbers_and_bounds(
|
||||
pattern: str,
|
||||
text: str,
|
||||
*,
|
||||
offset_to_line_cache: dict[int, int] | None = None,
|
||||
flags: int = 0,
|
||||
) -> Iterator[tuple[_re.Match[str], int, tuple[int, int]]]:
|
||||
"""
|
||||
A version of ``re.finditer`` that returns ``(match, line_number, line_bounds)``.
|
||||
|
||||
Note that ``offset_to_line_cache`` is the first return value from
|
||||
``finditer_newline_cache_compute``.
|
||||
This should be passed in if the iterator is called multiple times
|
||||
on the same buffer, to avoid calculating this every time.
|
||||
"""
|
||||
if offset_to_line_cache is None:
|
||||
offset_to_line_cache, line_to_offset_cache = finditer_newline_cache_compute(text)
|
||||
del line_to_offset_cache
|
||||
|
||||
text_len = len(text)
|
||||
for m in _re.finditer(pattern, text, flags):
|
||||
|
||||
if (beg := text.rfind("\n", 0, m.start())) == -1:
|
||||
beg = 0
|
||||
line_number = 0
|
||||
else:
|
||||
line_number = offset_to_line_cache[beg]
|
||||
|
||||
if (end := text.find("\n", m.end(), text_len)) == -1:
|
||||
end = text_len
|
||||
|
||||
yield m, line_number, (beg, end)
|
||||
|
||||
|
||||
def line_to_offset_range(line: int, offset_limit: int, line_to_offset_cache: list[int]) -> tuple[int, int]:
|
||||
"""
|
||||
Given an offset, return line bounds.
|
||||
"""
|
||||
assert line >= 0
|
||||
beg = line_to_offset_cache[line]
|
||||
end = line_to_offset_cache[line + 1] if (line + 1 < len(line_to_offset_cache)) else offset_limit
|
||||
return beg, end
|
||||
110
blender-5.2.0/tools/utils_maintenance/trailing_space_clean.py
Executable file
110
blender-5.2.0/tools/utils_maintenance/trailing_space_clean.py
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from trailing_space_clean_config import PATHS
|
||||
|
||||
SOURCE_EXT = (
|
||||
# C/C++
|
||||
".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl",
|
||||
# Objective C
|
||||
".m", ".mm",
|
||||
# GLSL
|
||||
".glsl",
|
||||
# Python
|
||||
".py",
|
||||
# TOML.
|
||||
".toml",
|
||||
# Text (also CMake)
|
||||
".txt", ".cmake", ".rst",
|
||||
# MS-Windows Scripts.
|
||||
".bat", ".cmd",
|
||||
# HTML, XML.
|
||||
".html",
|
||||
".xml",
|
||||
)
|
||||
|
||||
|
||||
def is_source(filename: str) -> bool:
|
||||
return filename.endswith(SOURCE_EXT)
|
||||
|
||||
|
||||
def path_iter(
|
||||
path: str,
|
||||
filename_check: Callable[[str], bool] | None = None,
|
||||
) -> Iterator[str]:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
# skip ".git"
|
||||
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
||||
|
||||
for filename in filenames:
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
filepath = join(dirpath, filename)
|
||||
if filename_check is None or filename_check(filepath):
|
||||
yield filepath
|
||||
|
||||
|
||||
def path_expand(
|
||||
paths: Sequence[str],
|
||||
filename_check: Callable[[str], bool] | None = None,
|
||||
) -> Iterator[str]:
|
||||
for f in paths:
|
||||
if not os.path.exists(f):
|
||||
print("Missing:", f)
|
||||
elif os.path.isdir(f):
|
||||
yield from path_iter(f, filename_check)
|
||||
else:
|
||||
yield f
|
||||
|
||||
|
||||
def rstrip_file(filename: str) -> tuple[str, ...]:
|
||||
reports = []
|
||||
with open(filename, "r", encoding="utf-8") as fh:
|
||||
data_src = fh.read()
|
||||
|
||||
# Strip trailing space.
|
||||
data_dst_list = []
|
||||
for l in data_src.rstrip().splitlines(True):
|
||||
data_dst_list.append(l.rstrip() + "\n")
|
||||
|
||||
data_dst = "".join(data_dst_list)
|
||||
del data_dst_list
|
||||
|
||||
# Remove BOM.
|
||||
if data_dst and (data_dst[0] == '\ufeff'):
|
||||
data_dst = data_dst[1:]
|
||||
|
||||
len_strip = len(data_src) - len(data_dst)
|
||||
if len_strip != 0:
|
||||
reports.append("STRIP={:d}".format(len_strip))
|
||||
|
||||
if len_strip:
|
||||
with open(filename, "w", encoding="utf-8") as fh:
|
||||
fh.write(data_dst)
|
||||
return tuple(reports)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for f in path_expand(PATHS, is_source):
|
||||
report = rstrip_file(f)
|
||||
if report:
|
||||
print("Strip ({:s}): {:s}".format(', '.join(report), f))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"PATHS",
|
||||
"SOURCE_DIR",
|
||||
)
|
||||
|
||||
|
||||
import os
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterator,
|
||||
)
|
||||
|
||||
|
||||
PATHS: tuple[str, ...] = (
|
||||
"build_files/build_environment/cmake",
|
||||
"build_files/cmake",
|
||||
"doc/python_api",
|
||||
"intern/clog",
|
||||
"intern/cycles",
|
||||
"intern/ghost",
|
||||
"intern/guardedalloc",
|
||||
"intern/memutil",
|
||||
"scripts/modules",
|
||||
"scripts",
|
||||
"source",
|
||||
"tests",
|
||||
|
||||
# files
|
||||
"GNUmakefile",
|
||||
"make.bat",
|
||||
)
|
||||
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(
|
||||
os.path.join(os.path.dirname(__file__), "..", ".."))))
|
||||
|
||||
PATHS = tuple(
|
||||
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
|
||||
for p in PATHS
|
||||
)
|
||||
|
||||
|
||||
def files(path: str, test_fn: Callable[[str], bool]) -> Iterator[str]:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
# skip '.git'
|
||||
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
||||
for filename in filenames:
|
||||
if test_fn(filename):
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
yield filepath
|
||||
|
||||
|
||||
PATHS = PATHS + tuple(
|
||||
files(
|
||||
os.path.join(SOURCE_DIR),
|
||||
lambda filename: filename in {"CMakeLists.txt"} or filename.endswith((".cmake"))
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user