Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View 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())

View File

@@ -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()

View File

@@ -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()

View 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())

View 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())

View 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())

View 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()

View 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())

File diff suppressed because it is too large Load Diff

View 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()

View File

@@ -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)

View File

@@ -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

View 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()

View File

@@ -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"))
)
)