Add Chromium-only Blender WebEngine parity work
This commit is contained in:
291
blender-5.2.0/tools/utils/addr2line_backtrace.py
Executable file
291
blender-5.2.0/tools/utils/addr2line_backtrace.py
Executable file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Extract line & function information from addresses (found in back-traces) using addr2line.
|
||||
|
||||
Example:
|
||||
|
||||
addr2line_backtrace.py --exe=/path/to/blender error.log
|
||||
|
||||
Piping from the standard-input is also supported:
|
||||
|
||||
cat error.log | addr2line_backtrace.py --exe=blender.bin
|
||||
|
||||
The text is printed to the standard output.
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from collections.abc import (
|
||||
Sequence,
|
||||
)
|
||||
|
||||
RE_ADDR = re.compile("\\[(0x[A-Fa-f0-9]+)\\]")
|
||||
IS_ATTY = sys.stdout.isatty()
|
||||
|
||||
|
||||
def value_as_percentage(value_partial: int, value_final: int) -> str:
|
||||
percent = 0.0 if (value_final == 0) else (value_partial / value_final)
|
||||
return "{:-6.2f}%".format(percent * 100)
|
||||
|
||||
|
||||
if IS_ATTY:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("\r\033[K[{:s}]: {:s}".format(value_as_percentage(value_partial, value_final), info))
|
||||
else:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("[{:s}]: {:s}\n".format(value_as_percentage(value_partial, value_final), info))
|
||||
|
||||
|
||||
def find_gitroot(filepath_reference: str) -> str | None:
|
||||
path = filepath_reference
|
||||
path_prev = ""
|
||||
found = False
|
||||
while not (found := os.path.exists(os.path.join(path, ".git"))) and path != path_prev:
|
||||
path_prev = path
|
||||
path = os.path.dirname(path)
|
||||
if found:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def addr2line_fn(arg_pair: tuple[tuple[str, str, bool], Sequence[str]]) -> Sequence[tuple[str, str]]:
|
||||
shared_args, addr_list = arg_pair
|
||||
(exe, base_path, time_command) = shared_args
|
||||
cmd = (
|
||||
"addr2line",
|
||||
*addr_list,
|
||||
"--functions",
|
||||
"--demangle",
|
||||
"--exe=" + exe,
|
||||
)
|
||||
if time_command:
|
||||
time_beg = time.time()
|
||||
|
||||
output = subprocess.check_output(cmd).rstrip().decode("utf-8", errors="surrogateescape")
|
||||
output_lines = output.split("\n")
|
||||
|
||||
result: list[tuple[str, str]] = []
|
||||
|
||||
while output_lines:
|
||||
# Swap (function, line), to (line, function).
|
||||
output_lines_for_addr = output_lines[:2]
|
||||
assert len(output_lines_for_addr) == 2
|
||||
del output_lines[:2]
|
||||
line_list = []
|
||||
for line in output_lines_for_addr:
|
||||
if base_path and line.startswith(base_path):
|
||||
line = "." + os.sep + line[len(base_path):]
|
||||
line_list.append(line)
|
||||
output = ": ".join(reversed(line_list))
|
||||
|
||||
if time_command:
|
||||
time_end = time.time()
|
||||
output = "{:s} ({:.2f})".format(output, time_end - time_beg)
|
||||
result.append((addr_list[len(result)], output))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
# When `--help` or no arguments are given, print this help.
|
||||
epilog = "This is typically used from the output of a stack-trace on Linux/Unix."
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
description=__doc__,
|
||||
epilog=epilog,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--exe",
|
||||
dest="exe",
|
||||
metavar='EXECUTABLE',
|
||||
required=True,
|
||||
help="Path to the binary.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base",
|
||||
dest="base",
|
||||
metavar='BASE_PATH',
|
||||
default="",
|
||||
required=False,
|
||||
help="Base path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--time",
|
||||
dest="time_command",
|
||||
action='store_true',
|
||||
required=False,
|
||||
help="Time addr2line (useful for checking on especially slow lookup).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
dest="jobs",
|
||||
type=int,
|
||||
default=4,
|
||||
help=(
|
||||
"The number of processes to use. "
|
||||
"Defaults to 4 to prevent using too much memory, 1 is single threaded (useful for debugging)."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-last",
|
||||
dest="skip_last",
|
||||
type=int,
|
||||
default=0,
|
||||
required=False,
|
||||
help=(
|
||||
"Optionally skip the last N addresses found.\n"
|
||||
"You may want to set this to 1 as the first address\n"
|
||||
"may point to an address that calls the main() function."
|
||||
"\n"
|
||||
"In this case the lookup will fail, taking significantly longer than all other lookups.\n"
|
||||
"Typically values between 1-4 should be used."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"backtraces",
|
||||
nargs="*",
|
||||
help="Back-trace files to scan for addresses.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def backtraces_extract_from_text(backtrace_data: str) -> set[str]:
|
||||
addr_set = set()
|
||||
for match in RE_ADDR.finditer(backtrace_data):
|
||||
addr = match.group(1)
|
||||
addr_set.add(addr)
|
||||
return addr_set
|
||||
|
||||
|
||||
def backtraces_extract_from_text_skip_last(
|
||||
backtrace_data: str,
|
||||
skip_last: int,
|
||||
) -> tuple[set[str], set[str]]:
|
||||
addr_set = set()
|
||||
addr_list = []
|
||||
for match in RE_ADDR.finditer(backtrace_data):
|
||||
addr = match.group(1)
|
||||
addr_set.add(addr)
|
||||
addr_list.append(addr)
|
||||
|
||||
# Remove the last N items.
|
||||
addr_set_skip = set()
|
||||
for i, addr in enumerate(reversed(addr_list)):
|
||||
if i == skip_last:
|
||||
break
|
||||
# Unlikely but possible, probably too many values are being skipped.
|
||||
if addr in addr_set_skip:
|
||||
skip_last += 1
|
||||
continue
|
||||
addr_set_skip.add(addr)
|
||||
addr_set.remove(addr)
|
||||
|
||||
return addr_set, addr_set_skip
|
||||
|
||||
|
||||
def addr2line_for_filedata(
|
||||
exe: str,
|
||||
base_path: str,
|
||||
time_command: bool,
|
||||
skip_last: int,
|
||||
jobs: int,
|
||||
backtrace_data: str,
|
||||
) -> None:
|
||||
|
||||
if skip_last == 0:
|
||||
addr_set = backtraces_extract_from_text(backtrace_data)
|
||||
addr_set_skip: set[str] = set()
|
||||
else:
|
||||
addr_set, addr_set_skip = backtraces_extract_from_text_skip_last(backtrace_data, skip_last)
|
||||
|
||||
shared_args = exe, base_path, time_command
|
||||
if jobs >= len(addr_set):
|
||||
addr2line_args = [(shared_args, [addr]) for addr in addr_set]
|
||||
else:
|
||||
addr2line_args = [(shared_args, []) for _ in range(jobs)]
|
||||
# Avoid using consecutive addresses in chunks since slower lookups are likely to be groups.
|
||||
for i, addr in enumerate(addr_set):
|
||||
addr2line_args[i % jobs][1].append(addr)
|
||||
|
||||
addr_map = {}
|
||||
addr_done = 0
|
||||
addr_len = len(addr_set)
|
||||
|
||||
with multiprocessing.Pool(jobs) as pool:
|
||||
for i, result_list in enumerate(pool.imap_unordered(addr2line_fn, addr2line_args), 1):
|
||||
for (addr, result) in result_list:
|
||||
if addr_set_skip:
|
||||
skip_text = " (skipped {:d})".format(len(addr_set_skip))
|
||||
else:
|
||||
skip_text = ""
|
||||
progress_output(addr_done, addr_len, "{:d} of {:d}{:s}".format(addr_done, addr_len, skip_text))
|
||||
del skip_text
|
||||
|
||||
addr_map[addr] = result
|
||||
addr_done += 1
|
||||
|
||||
if IS_ATTY:
|
||||
print()
|
||||
|
||||
def re_replace_fn(match: re.Match[str]) -> str:
|
||||
addr = match.group(1)
|
||||
if addr in addr_set_skip:
|
||||
return "{:s} ({:s})".format("<SKIP>", addr)
|
||||
return "{:s} ({:s})".format(addr_map[addr], addr)
|
||||
|
||||
backtrace_data_updated = RE_ADDR.sub(re_replace_fn, backtrace_data)
|
||||
|
||||
sys.stdout.write(backtrace_data_updated)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
jobs = args.jobs
|
||||
if jobs <= 0:
|
||||
jobs = multiprocessing.cpu_count()
|
||||
|
||||
base_path = args.base
|
||||
if not base_path:
|
||||
base_test = find_gitroot(os.getcwd())
|
||||
if base_test is not None:
|
||||
base_path = base_test
|
||||
if base_path:
|
||||
base_path = base_path.rstrip(os.sep) + os.sep
|
||||
|
||||
if args.backtraces:
|
||||
for backtrace_filepath in args.backtraces:
|
||||
try:
|
||||
with open(backtrace_filepath, 'r', encoding="utf-8", errors="surrogateescape") as fh:
|
||||
bactrace_data = fh.read()
|
||||
except Exception as ex:
|
||||
print("Filed to open {!r}, {:s}".format(backtrace_filepath, str(ex)))
|
||||
continue
|
||||
|
||||
addr2line_for_filedata(args.exe, base_path, args.time_command, args.skip_last, jobs, bactrace_data)
|
||||
else:
|
||||
bactrace_data = sys.stdin.read()
|
||||
addr2line_for_filedata(args.exe, base_path, args.time_command, args.skip_last, jobs, bactrace_data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
458
blender-5.2.0/tools/utils/authors_git_gen.py
Executable file
458
blender-5.2.0/tools/utils/authors_git_gen.py
Executable file
@@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Example use to generate all authors:
|
||||
|
||||
authors_git_gen.py
|
||||
|
||||
Example use a custom range:
|
||||
|
||||
authors_git_gen.py --source=/src/blender --range=SHA1..HEAD
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
# NOTE: this shares the basic structure with `credits_git_gen.py`,
|
||||
# however details differ enough for them to be separate scripts.
|
||||
# Improvements to this script may apply there too.
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
from collections.abc import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
)
|
||||
|
||||
from git_log import (
|
||||
GitCommitIter,
|
||||
GitCommit,
|
||||
)
|
||||
|
||||
import git_data_canonical_authors
|
||||
import git_data_sha1_override_authors
|
||||
|
||||
IS_ATTY = sys.stdout.isatty()
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.join(BASE_DIR, "..", ".."))
|
||||
|
||||
# Exclude authors with this many lines modified.
|
||||
AUTHOR_LINES_SKIP = 4
|
||||
|
||||
# Stop counting after this line limit.
|
||||
AUTHOR_LINES_LIMIT = 100
|
||||
|
||||
AUTHOR_DIRECTORIES_SKIP: tuple[bytes, ...] = (
|
||||
b"blender/extern/",
|
||||
b"blender/intern/opennl/",
|
||||
# Will have own authors file.
|
||||
b"intern/cycles/",
|
||||
# Will have own authors file.
|
||||
b"intern/libmv/libmv/",
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Lookup Table to clean up the authors
|
||||
#
|
||||
# This is a combination of unifying git logs as well as
|
||||
# name change requested by the authors.
|
||||
|
||||
# Some projects prefer not to have their developers listed.
|
||||
author_exclude_individuals = {
|
||||
"Jason Fielder <jason-fielder@noreply.localhost>", # `@apple.com` developer.
|
||||
"LLM Assistant <llm-assistant>", # Set as a co-author for some commits.
|
||||
"Michael B Johnson <wave@noreply.localhost>", # `@apple.com` developer.
|
||||
}
|
||||
|
||||
author_exclude_glob = (
|
||||
"* <*@apple.com>",
|
||||
)
|
||||
|
||||
author_table = git_data_canonical_authors.canonical_author_map()
|
||||
author_override_table = git_data_sha1_override_authors.sha1_authors_map()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Multi-Processing
|
||||
|
||||
|
||||
def process_commits_for_map(commits: Iterable[GitCommit]) -> "Credits":
|
||||
result = Credits()
|
||||
for c in commits:
|
||||
result.process_commit(c)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Progress Display
|
||||
|
||||
def value_as_percentage(value_partial: int, value_final: int) -> str:
|
||||
percent = 0.0 if (value_final == 0) else (value_partial / value_final)
|
||||
return "{:-6.2f}%".format(percent * 100)
|
||||
|
||||
|
||||
if IS_ATTY:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("\r\033[K[{:s}]: {:s}".format(value_as_percentage(value_partial, value_final), info))
|
||||
else:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("[{:s}]: {:s}\n".format(value_as_percentage(value_partial, value_final), info))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class for generating authors
|
||||
|
||||
class CreditUser:
|
||||
__slots__ = (
|
||||
"commit_total",
|
||||
"lines_change",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.commit_total = 0
|
||||
self.lines_change = -1
|
||||
|
||||
|
||||
class Credits:
|
||||
__slots__ = (
|
||||
"users",
|
||||
# Use for progress, simply the number of times `process_commit` has been called.
|
||||
"process_commits_count",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.users: dict[str, CreditUser] = {}
|
||||
self.process_commits_count = 0
|
||||
|
||||
@classmethod
|
||||
def commit_authors_get(cls, c: GitCommit) -> list[str]:
|
||||
if (authors_overwrite := author_override_table.get(c.sha1, None)) is not None:
|
||||
# Ignore git commit info for these having an entry in `author_override_table`.
|
||||
return [author_table.get(author, author) for author in authors_overwrite]
|
||||
|
||||
authors = [c.author] + c.co_authors
|
||||
# Normalize author string into canonical form, prevents duplicate credit users
|
||||
authors = [unicodedata.normalize('NFC', author) for author in authors]
|
||||
return [author_table.get(author, author) for author in authors]
|
||||
|
||||
@classmethod
|
||||
def is_credit_commit_valid(cls, c: GitCommit) -> bool:
|
||||
if not any(f for f in c.files if not f.startswith(AUTHOR_DIRECTORIES_SKIP)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def merge(self, other: "Credits") -> None:
|
||||
"""
|
||||
Merge other Credits into this, clearing the other.
|
||||
"""
|
||||
for user_key, user_other in other.users.items():
|
||||
user = self.users.get(user_key)
|
||||
if user is None:
|
||||
# Consume the user.
|
||||
self.users[user_key] = user_other
|
||||
else:
|
||||
user.commit_total += user_other.commit_total
|
||||
user.lines_change += user_other.lines_change
|
||||
other.users.clear()
|
||||
|
||||
def process_commit(self, c: GitCommit) -> None:
|
||||
self.process_commits_count += 1
|
||||
|
||||
if not self.is_credit_commit_valid(c):
|
||||
return
|
||||
|
||||
lines_change = -1
|
||||
|
||||
authors = self.commit_authors_get(c)
|
||||
for author in authors:
|
||||
cu = self.users.get(author)
|
||||
if cu is None:
|
||||
cu = self.users[author] = CreditUser()
|
||||
|
||||
cu.commit_total += 1
|
||||
|
||||
lines_change_for_author = cu.lines_change
|
||||
if lines_change_for_author < AUTHOR_LINES_LIMIT:
|
||||
if lines_change_for_author == -1:
|
||||
cu.lines_change = lines_change_for_author = 0
|
||||
|
||||
if lines_change == -1:
|
||||
diff = c.diff
|
||||
lines_change = diff.count("\n-") + diff.count("\n+")
|
||||
|
||||
cu.lines_change += lines_change
|
||||
|
||||
def _process_multiprocessing(self, commit_iter: Iterable[GitCommit], *, jobs: int) -> None:
|
||||
print("Collecting commits...")
|
||||
# NOTE(@ideasman42): that the chunk size doesn't have as much impact on
|
||||
# performance as you might expect, values between 16 and 1024 seem reasonable.
|
||||
# Although higher values tend to bottleneck as the process finishes.
|
||||
chunk_size = 256
|
||||
chunk_list = []
|
||||
chunk = []
|
||||
for c in commit_iter:
|
||||
chunk.append(c)
|
||||
if len(chunk) >= chunk_size:
|
||||
chunk_list.append(chunk)
|
||||
chunk = []
|
||||
if chunk:
|
||||
chunk_list.append(chunk)
|
||||
|
||||
commit_count_total = (max(len(chunk_list) - 1, 0) * chunk_size) + len(chunk)
|
||||
|
||||
print("Found {:,d} commits, processing...".format(commit_count_total))
|
||||
commit_count_handled = 0
|
||||
with multiprocessing.Pool(processes=jobs) as pool:
|
||||
for result in pool.imap_unordered(process_commits_for_map, chunk_list):
|
||||
commit_count_handled += result.process_commits_count
|
||||
progress_output(
|
||||
commit_count_handled,
|
||||
commit_count_total,
|
||||
"{:,d} / {:,d} commits".format(commit_count_handled, commit_count_total),
|
||||
)
|
||||
self.merge(result)
|
||||
|
||||
if IS_ATTY:
|
||||
print("") # Was printing on one-line, move to next.
|
||||
|
||||
def process(self, commit_iter: Iterable[GitCommit], *, jobs: int) -> None:
|
||||
if jobs > 1:
|
||||
self._process_multiprocessing(commit_iter, jobs=jobs)
|
||||
return
|
||||
|
||||
# Simple single process operation.
|
||||
for i, c in enumerate(commit_iter):
|
||||
self.process_commit(c)
|
||||
if not (i % 100):
|
||||
print(i)
|
||||
|
||||
def iter_authors(
|
||||
self,
|
||||
*,
|
||||
use_metadata: bool = False,
|
||||
verbose: bool = False,
|
||||
) -> Iterator[str]:
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
commit_word = "commit", "commits"
|
||||
metadata_right_margin = 79
|
||||
|
||||
author_exclude_regex = tuple(
|
||||
(match_glob, re.compile(fnmatch.translate(match_glob)))
|
||||
for match_glob in author_exclude_glob
|
||||
)
|
||||
|
||||
# Normalize using decomposed strings for sorting.
|
||||
# That makes letters with diacritics grouped with the undiacritized variant.
|
||||
# Collation is not perfect, but better than having non-English letters at the very bottom of the list.
|
||||
from unicodedata import normalize
|
||||
sorted_authors = dict(sorted(
|
||||
self.users.items(),
|
||||
key=lambda item: (normalize('NFD', item[0]).casefold(), item[1].commit_total),
|
||||
))
|
||||
|
||||
for author_with_email, cu in sorted_authors.items():
|
||||
if author_with_email.endswith(" <>"):
|
||||
if verbose:
|
||||
print("Skipping:", author_with_email, "(no email)")
|
||||
continue
|
||||
if cu.lines_change <= AUTHOR_LINES_SKIP:
|
||||
if verbose:
|
||||
print("Skipping:", author_with_email, cu.lines_change, "line(s) changed.")
|
||||
continue
|
||||
if author_with_email in author_exclude_individuals:
|
||||
if verbose:
|
||||
print("Skipping:", author_with_email, "explicit exclusion requested.")
|
||||
continue
|
||||
if match_glob_found := next(iter([
|
||||
match_glob for match_glob, match_regex in author_exclude_regex
|
||||
if match_regex.match(author_with_email)
|
||||
]), None):
|
||||
if verbose:
|
||||
print("Skipping:", author_with_email, "glob exclusion \"{:s}\" requested.".format(match_glob_found))
|
||||
continue
|
||||
|
||||
if use_metadata:
|
||||
yield "{:s} {:s}# lines={:,d} ({:s}), {:,d} {:s}".format(
|
||||
author_with_email,
|
||||
(" " * max(1, metadata_right_margin - len(author_with_email))),
|
||||
min(cu.lines_change, AUTHOR_LINES_LIMIT),
|
||||
"" if cu.lines_change >= AUTHOR_LINES_LIMIT else "<SKIP?>",
|
||||
cu.commit_total,
|
||||
commit_word[cu.commit_total > 1],
|
||||
)
|
||||
else:
|
||||
yield author_with_email
|
||||
|
||||
def report_warnings(self) -> None:
|
||||
# Warn about authors that share an email field.
|
||||
emails: dict[str, list[str]] = {}
|
||||
for author in self.iter_authors(use_metadata=False):
|
||||
name, email = author.rstrip(">").rsplit(" <", 1)
|
||||
try:
|
||||
emails[email].append(name)
|
||||
except KeyError:
|
||||
emails[email] = [name]
|
||||
for email, names in emails.items():
|
||||
if len(names) > 1:
|
||||
print("Warning: multiple names for an email '{:s}' {!r}".format(email, names))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Argument Parser
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = "List authors."
|
||||
|
||||
epilog = "This script is used to generate an AUTHORS file"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
||||
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
dest="source_dir",
|
||||
metavar='PATH',
|
||||
required=False,
|
||||
default=SOURCE_DIR,
|
||||
help="Path to git repository",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
dest="output_filepath",
|
||||
metavar='PATH',
|
||||
required=False,
|
||||
default="",
|
||||
help="File path to write the output to (when omitted, update the \"AUTHORS\" file)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--range",
|
||||
dest="range_sha1",
|
||||
metavar='SHA1_RANGE',
|
||||
required=False,
|
||||
default="HEAD",
|
||||
help="Range to use, eg: 169c95b8..HEAD or HEAD for all history.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
dest="jobs",
|
||||
type=int,
|
||||
default=0,
|
||||
help=(
|
||||
"The number of processes to use. "
|
||||
"Defaults to zero which detects the available cores, 1 is single threaded (useful for debugging)."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--use-metadata",
|
||||
action='store_true',
|
||||
dest="use_metadata",
|
||||
default=False,
|
||||
help="Add commits & changed lines as metadata after the author."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-authors",
|
||||
action='store_true',
|
||||
dest="all_authors",
|
||||
default=False,
|
||||
help=(
|
||||
"Include all authors (don't skip based on paths or commit count/size). "
|
||||
"This can be useful when updating: "
|
||||
"'./tools/utils/git_data_canonical_authors.py' which is also used for credit generation."
|
||||
)
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main Function
|
||||
|
||||
def main() -> int:
|
||||
global AUTHOR_DIRECTORIES_SKIP
|
||||
global AUTHOR_LINES_SKIP
|
||||
global AUTHOR_LINES_LIMIT
|
||||
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
if args.all_authors:
|
||||
AUTHOR_DIRECTORIES_SKIP = ()
|
||||
AUTHOR_LINES_SKIP = 0
|
||||
AUTHOR_LINES_LIMIT = 0
|
||||
|
||||
credits = Credits()
|
||||
# commit_range = "HEAD~10..HEAD"
|
||||
# commit_range = "blender-v2.81-release..blender-v2.82-release"
|
||||
# commit_range = "blender-v2.82-release"
|
||||
commit_range = args.range_sha1
|
||||
jobs = args.jobs
|
||||
if jobs <= 0:
|
||||
# Clamp the value, higher values give errors with too many open files.
|
||||
# Allow users to manually pass very high values in as they might want to tweak system limits themselves.
|
||||
jobs = min(multiprocessing.cpu_count(), 400)
|
||||
|
||||
credits.process(GitCommitIter(args.source_dir, commit_range), jobs=jobs)
|
||||
|
||||
credits.report_warnings()
|
||||
|
||||
authors_text_io = io.StringIO()
|
||||
for author in credits.iter_authors(use_metadata=args.use_metadata, verbose=True):
|
||||
authors_text_io.write("{:s}\n".format(author))
|
||||
authors_text = authors_text_io.getvalue()
|
||||
del authors_text_io
|
||||
|
||||
if args.output_filepath:
|
||||
with open(args.output_filepath, 'w', encoding="utf8", errors='xmlcharrefreplace') as fh:
|
||||
fh.write(authors_text)
|
||||
print("Written:", args.output_filepath)
|
||||
return 0
|
||||
|
||||
filepath_authors = os.path.join(args.source_dir, "AUTHORS")
|
||||
|
||||
text_beg = "# BEGIN individuals section.\n"
|
||||
text_end = "# Please DO NOT APPEND here. See comments at the top of the file.\n"
|
||||
with open(filepath_authors, "r", encoding="utf-8") as fh:
|
||||
authors_contents = fh.read()
|
||||
|
||||
text_beg_index = authors_contents.index(text_beg)
|
||||
text_end_index = authors_contents.index(text_end)
|
||||
if text_beg_index == -1:
|
||||
print("Text: {!r} not found in {!r}".format(text_beg, filepath_authors))
|
||||
return 1
|
||||
if text_end_index == -1:
|
||||
print("Text: {!r} not found in {!r}".format(text_end, filepath_authors))
|
||||
return 1
|
||||
|
||||
text_beg_index += len(text_beg)
|
||||
|
||||
with open(filepath_authors, "w", encoding="utf-8") as fh:
|
||||
fh.write(authors_contents[:text_beg_index])
|
||||
fh.write(authors_text)
|
||||
fh.write(authors_contents[text_end_index:])
|
||||
|
||||
print("Updated:", filepath_authors)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
122
blender-5.2.0/tools/utils/autopep8_clean.py
Executable file
122
blender-5.2.0/tools/utils/autopep8_clean.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from autopep8_clean_config import PATHS, PATHS_EXCLUDE
|
||||
|
||||
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
# Useful to disable when debugging warnings.
|
||||
USE_MULTIPROCESS = True
|
||||
|
||||
print(PATHS)
|
||||
SOURCE_EXT = (
|
||||
# Python
|
||||
".py",
|
||||
)
|
||||
|
||||
|
||||
def is_source_and_included(filename: str) -> bool:
|
||||
return (
|
||||
filename.endswith(SOURCE_EXT) and
|
||||
filename not in PATHS_EXCLUDE
|
||||
)
|
||||
|
||||
|
||||
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 autopep8_format_file(f: str) -> None:
|
||||
print(f)
|
||||
subprocess.call((
|
||||
"autopep8",
|
||||
"--ignore",
|
||||
",".join((
|
||||
# Info: Use `isinstance()` instead of comparing types directly.
|
||||
# Why disable?: Changes code logic, in rare cases we want to compare exact types.
|
||||
"E721",
|
||||
# Info: Fix bare except.
|
||||
# Why disable?: Disruptive, leave our exceptions alone.
|
||||
"E722",
|
||||
# Info: Fix module level import not at top of file.
|
||||
# Why disable?: re-ordering imports is disruptive and breaks some scripts
|
||||
# that need to check if a module has already been loaded in the case of reloading.
|
||||
"E402",
|
||||
# Info: Try to make lines fit within --max-line-length characters.
|
||||
# Why disable? Causes lines to be wrapped, where long lines have the
|
||||
# trailing bracket moved to the end of the line.
|
||||
# If trailing commas were respected as they are by clang-format this might be acceptable.
|
||||
# Note that this doesn't disable all line wrapping.
|
||||
"E501",
|
||||
# Info: Fix various deprecated code (via lib2to3)
|
||||
# Why disable?: causes imports to be added/re-arranged.
|
||||
"W690",
|
||||
)),
|
||||
"--aggressive",
|
||||
"--in-place",
|
||||
"--max-line-length", "120",
|
||||
f,
|
||||
))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import sys
|
||||
|
||||
if os.path.samefile(sys.argv[-1], __file__):
|
||||
paths = path_expand(PATHS, is_source_and_included)
|
||||
else:
|
||||
paths = path_expand(sys.argv[1:], is_source_and_included)
|
||||
|
||||
if USE_MULTIPROCESS:
|
||||
import multiprocessing
|
||||
job_total = multiprocessing.cpu_count()
|
||||
pool = multiprocessing.Pool(processes=job_total)
|
||||
pool.map(autopep8_format_file, paths)
|
||||
else:
|
||||
for f in paths:
|
||||
autopep8_format_file(f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
45
blender-5.2.0/tools/utils/autopep8_clean_config.py
Normal file
45
blender-5.2.0/tools/utils/autopep8_clean_config.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"PATHS",
|
||||
"PATHS_EXCLUDE",
|
||||
"SOURCE_DIR",
|
||||
)
|
||||
|
||||
import os
|
||||
|
||||
PATHS: tuple[str, ...] = (
|
||||
"build_files",
|
||||
"doc",
|
||||
"release/datafiles",
|
||||
"release/lts",
|
||||
"scripts/freestyle",
|
||||
"scripts/modules",
|
||||
"scripts/presets",
|
||||
"scripts/startup",
|
||||
"scripts/templates_py",
|
||||
"source/blender",
|
||||
"tools",
|
||||
"tests",
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
PATHS_EXCLUDE: set[str] = set(
|
||||
os.path.join(SOURCE_DIR, p.replace("/", os.sep))
|
||||
for p in
|
||||
(
|
||||
"tools/svn_rev_map/sha1_to_rev.py",
|
||||
"tools/svn_rev_map/rev_to_sha1.py",
|
||||
"tools/svn_rev_map/rev_to_sha1.py",
|
||||
"scripts/modules/_rna_manual_reference.py",
|
||||
)
|
||||
)
|
||||
450
blender-5.2.0/tools/utils/blend2json.py
Executable file
450
blender-5.2.0/tools/utils/blend2json.py
Executable file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2015 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This is a tool for generating a JSON version of a blender file (only its structure, or all its data included).
|
||||
|
||||
It can also run some simple validity checks over a .blend file.
|
||||
|
||||
WARNING! This is still WIP tool!
|
||||
|
||||
Example usage:
|
||||
|
||||
./blend2json.py foo.blend
|
||||
|
||||
To output also all 'name' fields from data:
|
||||
|
||||
./blend2json.py --filter-data="name" foo.blend
|
||||
|
||||
To output complete DNA struct info:
|
||||
|
||||
./blend2json.py --full-dna foo.blend
|
||||
|
||||
To avoid getting all 'uid' old addresses (those will change really often even when data itself does not change,
|
||||
making diff pretty noisy):
|
||||
|
||||
./blend2json.py --no-old-addresses foo.blend
|
||||
|
||||
To check a .blend file instead of outputting its JSON version (use explicit -o option to do both at the same time):
|
||||
|
||||
./blend2json.py -c foo.blend
|
||||
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
FILTER_DOC = """
|
||||
Each generic filter is made of three arguments, the include/exclude toggle ('+'/'-'), a regex to match against the name
|
||||
of the field to check (either one of the 'meta-data' generated by json exporter, or actual data field from DNA structs),
|
||||
and some regex to match against the data of this field (JSON-ified representation of the data, hence always a string).
|
||||
|
||||
Filters are evaluated in the order they are given, that is, if a block does not pass the first filter,
|
||||
it is immediately rejected and no further check is done on it.
|
||||
|
||||
You can add some recursivity to a filter (that is, if an 'include' filter is successful over a 'pointer' property,
|
||||
it will also automatically include pointed data, with a level of recursivity), by adding either
|
||||
'*' (for infinite recursion) or a number (to specify the maximum level of recursion) to the include/exclude toggle.
|
||||
Note that it only makes sense in 'include' case, and gets ignored for 'exclude' one.
|
||||
|
||||
Examples:
|
||||
|
||||
To include only MESH blocks:
|
||||
|
||||
./blend2json.py --filter-block "+" "code" "ME" foo.blend
|
||||
|
||||
To include only MESH or CURVE blocks and all data used by them:
|
||||
|
||||
./blend2json.py --filter-block "+" "code" "(ME)|(CU)" --filter-block "+*" ".*" ".*" foo.blend
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
# Avoid maintaining multiple blendfile modules
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
|
||||
del sys
|
||||
|
||||
import blendfile
|
||||
|
||||
|
||||
##### Utils (own json formatting) #####
|
||||
|
||||
|
||||
def json_default(o):
|
||||
if isinstance(o, bytes):
|
||||
return repr(o)[2:-1]
|
||||
elif o is ...:
|
||||
return "<...>"
|
||||
return o
|
||||
|
||||
|
||||
def json_dumps(i):
|
||||
return json.dumps(i, default=json_default)
|
||||
|
||||
|
||||
def keyval_to_json(kvs, indent, indent_step, compact_output=False):
|
||||
if compact_output:
|
||||
return ('{' + ', '.join('"%s": %s' % (k, v) for k, v in kvs) + '}')
|
||||
else:
|
||||
return ('{%s' % indent_step[:-1] +
|
||||
(',\n%s%s' % (indent, indent_step)).join(
|
||||
('"%s":\n%s%s%s' % (k, indent, indent_step, v) if (v and v[0] in {'[', '{'}) else
|
||||
'"%s": %s' % (k, v)) for k, v in kvs) +
|
||||
'\n%s}' % indent)
|
||||
|
||||
|
||||
def list_to_json(lst, indent, indent_step, compact_output=False):
|
||||
if compact_output:
|
||||
return ('[' + ', '.join(l for l in lst) + ']')
|
||||
else:
|
||||
return ('[%s' % indent_step[:-1] +
|
||||
((',\n%s%s' % (indent, indent_step)).join(
|
||||
('\n%s%s%s' % (indent, indent_step, l) if (i == 0 and l[0] in {'[', '{'}) else l)
|
||||
for i, l in enumerate(lst))
|
||||
) +
|
||||
'\n%s]' % indent)
|
||||
|
||||
|
||||
##### Main 'struct' writers #####
|
||||
|
||||
def gen_fake_addresses(args, blend):
|
||||
if args.use_fake_address:
|
||||
hashes = set()
|
||||
ret = {}
|
||||
hash_seed = 1
|
||||
for block in blend.blocks:
|
||||
if not block.addr_old:
|
||||
continue
|
||||
hsh = block.get_data_hash(hash_seed)
|
||||
while hsh in hashes:
|
||||
hsh += 1
|
||||
hashes.add(hsh)
|
||||
ret[block.addr_old] = hsh
|
||||
if (args.raw_bblock):
|
||||
hash_seed += 1
|
||||
return ret
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def bheader_to_json(args, fw, blend, indent, indent_step):
|
||||
fw('%s"%s": [\n' % (indent, "HEADER"))
|
||||
indent = indent + indent_step
|
||||
|
||||
keyval = (
|
||||
("magic", json_dumps(blend.header.magic)),
|
||||
("pointer_size", json_dumps(blend.header.pointer_size)),
|
||||
("is_little_endian", json_dumps(blend.header.is_little_endian)),
|
||||
("version", json_dumps(blend.header.version)),
|
||||
)
|
||||
keyval = keyval_to_json(keyval, indent, indent_step)
|
||||
fw('%s%s' % (indent, keyval))
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def do_bblock_filter(filters, blend, block, meta_keyval, data_keyval):
|
||||
def do_bblock_filter_data_recursive(blend, block, rec_lvl, rec_iter, key=None):
|
||||
fields = (blend.structs[block.sdna_index].fields if key is None else
|
||||
[blend.structs[block.sdna_index].field_from_name.get(key[1:-1].encode())])
|
||||
for fld in fields:
|
||||
if fld is None:
|
||||
continue
|
||||
if fld.dna_name.is_pointer:
|
||||
paths = ([(fld.dna_name.name_only, i) for i in range(fld.dna_name.array_size)]
|
||||
if fld.dna_name.array_size > 1 else [fld.dna_name.name_only])
|
||||
for p in paths:
|
||||
child_block = block.get_pointer(p)
|
||||
if child_block is not None:
|
||||
child_block.user_data = max(block.user_data, rec_iter)
|
||||
if rec_lvl != 0:
|
||||
do_bblock_filter_data_recursive(blend, child_block, rec_lvl - 1, rec_iter + 1)
|
||||
|
||||
has_include = False
|
||||
do_break = False
|
||||
rec_iter = 1
|
||||
if block.user_data is None:
|
||||
block.user_data = 0
|
||||
for include, rec_lvl, key, val in filters:
|
||||
if rec_lvl < 0:
|
||||
rec_lvl = 100
|
||||
has_include = has_include or include
|
||||
# Skip exclude filters if block was already processed some way.
|
||||
if not include and block.user_data is not None:
|
||||
continue
|
||||
has_match = False
|
||||
for k, v in meta_keyval:
|
||||
if key.search(k) and val.search(v):
|
||||
has_match = True
|
||||
if include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
|
||||
# ones may still have to be processed...
|
||||
else:
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
break
|
||||
for k, v in data_keyval:
|
||||
if key.search(k) and val.search(v):
|
||||
has_match = True
|
||||
if include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
if rec_lvl != 0:
|
||||
do_bblock_filter_data_recursive(blend, block, rec_lvl - 1, rec_iter + 1, k)
|
||||
# Note that in include cases, we have to keep checking filters, since some 'include recursive'
|
||||
# ones may still have to be processed...
|
||||
else:
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
break
|
||||
if include and not has_match: # Include check failed, implies exclusion.
|
||||
block.user_data = min(block.user_data, -rec_iter)
|
||||
do_break = True # No need to check more filters in exclude case...
|
||||
if do_break:
|
||||
break
|
||||
# Implicit 'include all' in case no include filter is specified...
|
||||
if block.user_data == 0 and not has_include:
|
||||
block.user_data = max(block.user_data, rec_iter)
|
||||
|
||||
|
||||
def bblocks_to_json(args, fw, blend, address_map, indent, indent_step):
|
||||
raw_bblock = args.raw_bblock
|
||||
no_address = args.no_address
|
||||
full_data = args.full_data
|
||||
filter_data = args.filter_data
|
||||
|
||||
def gen_meta_keyval(blend, block):
|
||||
keyval = [
|
||||
("code", json_dumps(block.code)),
|
||||
("size", json_dumps(block.size)),
|
||||
("file_offset", json_dumps(block.file_offset)),
|
||||
]
|
||||
if not no_address:
|
||||
keyval += [("addr_old", json_dumps(address_map.get(block.addr_old, block.addr_old)))]
|
||||
if raw_bblock:
|
||||
keyval += [("dna_index", json_dumps(block.sdna_index))]
|
||||
else:
|
||||
keyval += [("dna_type_id", json_dumps(blend.structs[block.sdna_index].dna_type_id))]
|
||||
keyval += [("count", json_dumps(block.count))]
|
||||
return keyval
|
||||
|
||||
def gen_data_keyval(blend, block, key_filter=None):
|
||||
if raw_bblock:
|
||||
return []
|
||||
|
||||
def _is_pointer(k):
|
||||
return blend.structs[block.sdna_index].field_from_path(blend.header, blend.handle, k).dna_name.is_pointer
|
||||
if key_filter is not None:
|
||||
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
|
||||
for k, v in block.items_recursive_iter() if k in key_filter]
|
||||
return [(json_dumps(k)[1:-1], json_dumps(address_map.get(v, v) if _is_pointer(k) else v))
|
||||
for k, v in block.items_recursive_iter()]
|
||||
|
||||
if args.block_filters:
|
||||
for block in blend.blocks:
|
||||
meta_keyval = gen_meta_keyval(blend, block)
|
||||
data_keyval = gen_data_keyval(blend, block)
|
||||
do_bblock_filter(args.block_filters, blend, block, meta_keyval, data_keyval)
|
||||
|
||||
fw('%s"%s": [\n' % (indent, "DATA"))
|
||||
indent = indent + indent_step
|
||||
|
||||
is_first = True
|
||||
for block in blend.blocks:
|
||||
if block.user_data is None or block.user_data > 0:
|
||||
meta_keyval = gen_meta_keyval(blend, block)
|
||||
if full_data:
|
||||
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block),
|
||||
indent + indent_step, indent_step, args.compact_output)))
|
||||
elif filter_data:
|
||||
meta_keyval.append(("data", keyval_to_json(gen_data_keyval(blend, block, filter_data),
|
||||
indent + indent_step, indent_step, args.compact_output)))
|
||||
keyval = keyval_to_json(meta_keyval, indent, indent_step, args.compact_output)
|
||||
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
|
||||
is_first = False
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def bdna_to_json(args, fw, blend, indent, indent_step):
|
||||
full_dna = args.full_dna and not args.compact_output
|
||||
|
||||
def bdna_fields_to_json(blend, dna, indent, indent_step):
|
||||
lst = []
|
||||
for field in dna.fields:
|
||||
keyval = (
|
||||
("dna_name", json_dumps(field.dna_name.name_only)),
|
||||
("dna_type_id", json_dumps(field.dna_type.dna_type_id)),
|
||||
("is_pointer", json_dumps(field.dna_name.is_pointer)),
|
||||
("is_method_pointer", json_dumps(field.dna_name.is_method_pointer)),
|
||||
("array_size", json_dumps(field.dna_name.array_size)),
|
||||
)
|
||||
lst.append(keyval_to_json(keyval, indent + indent_step, indent_step))
|
||||
return list_to_json(lst, indent, indent_step)
|
||||
|
||||
fw('%s"%s": [\n' % (indent, "DNA_STRUCT"))
|
||||
indent = indent + indent_step
|
||||
|
||||
is_first = True
|
||||
for dna in blend.structs:
|
||||
keyval = [
|
||||
("dna_type_id", json_dumps(dna.dna_type_id)),
|
||||
("size", json_dumps(dna.size)),
|
||||
]
|
||||
if full_dna:
|
||||
keyval += [("fields", bdna_fields_to_json(blend, dna, indent + indent_step, indent_step))]
|
||||
else:
|
||||
keyval += [("nbr_fields", json_dumps(len(dna.fields)))]
|
||||
keyval = keyval_to_json(keyval, indent, indent_step, args.compact_output)
|
||||
fw('%s%s%s' % ('' if is_first else ',\n', indent, keyval))
|
||||
is_first = False
|
||||
|
||||
indent = indent[:-len(indent_step)]
|
||||
fw('\n%s]' % indent)
|
||||
|
||||
|
||||
def blend_to_json(args, f, blend, address_map):
|
||||
fw = f.write
|
||||
fw('{\n')
|
||||
indent = indent_step = " "
|
||||
bheader_to_json(args, fw, blend, indent, indent_step)
|
||||
fw(',\n')
|
||||
bblocks_to_json(args, fw, blend, address_map, indent, indent_step)
|
||||
if not args.raw_bblock:
|
||||
fw(',\n')
|
||||
bdna_to_json(args, fw, blend, indent, indent_step)
|
||||
fw('\n}\n')
|
||||
|
||||
|
||||
##### Checks #####
|
||||
|
||||
def check_file(args, blend):
|
||||
addr_old = set()
|
||||
for block in blend.blocks:
|
||||
if block.addr_old in addr_old:
|
||||
print("ERROR! Several data blocks share same 'addr_old' UUID %d, "
|
||||
"this should never happen!" % block.addr_old)
|
||||
continue
|
||||
addr_old.add(block.addr_old)
|
||||
|
||||
|
||||
##### Main #####
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
global __doc__
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = __doc__
|
||||
|
||||
epilog = "This script is typically used to check differences between .blend files, or to check their validity."
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_argument(
|
||||
dest="input", nargs="+", metavar='PATH',
|
||||
help="Input .blend file(s)")
|
||||
parser.add_argument(
|
||||
"-o", "--output", dest="output", action="append", metavar='PATH', required=False,
|
||||
help="Output .json file(s) (same path/name as input file(s) if not specified)")
|
||||
parser.add_argument(
|
||||
"-c", "--check-file", dest="check_file", default=False, action='store_true', required=False,
|
||||
help=("Perform some basic validation checks over the .blend file"))
|
||||
parser.add_argument(
|
||||
"--compact-output", dest="compact_output", default=False, action='store_true', required=False,
|
||||
help=("Output a very compact representation of blendfile (one line per block/DNAStruct)"))
|
||||
parser.add_argument(
|
||||
"--no-old-addresses", dest="no_address", default=False, action='store_true', required=False,
|
||||
help=("Do not output old memory address of each block of data "
|
||||
"(used as 'UUID' in .blend files, but change pretty noisily)"))
|
||||
parser.add_argument(
|
||||
"--no-fake-old-addresses", dest="use_fake_address", default=True, action='store_false',
|
||||
required=False,
|
||||
help=("Do not 'rewrite' old memory address of each block of data "
|
||||
"(they are rewritten by default to some hash of their content, "
|
||||
"to try to avoid too much diff noise between different but similar files)"))
|
||||
parser.add_argument(
|
||||
"--full-data", dest="full_data",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also put in JSON file data itself "
|
||||
"(WARNING! will generate *huge* verbose files - and is far from complete yet)"))
|
||||
parser.add_argument(
|
||||
"--filter-data", dest="filter_data",
|
||||
default=None, required=False,
|
||||
help=("Only put in JSON file data fields which names match given comma-separated list "
|
||||
"(ignored if --full-data is set)"))
|
||||
parser.add_argument(
|
||||
"--full-dna", dest="full_dna", default=False, action='store_true', required=False,
|
||||
help=("Also put in JSON file dna properties description (ignored when --compact-output is used)"))
|
||||
|
||||
parser.add_argument(
|
||||
"--raw-bblock", dest="raw_bblock",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Do not attempt to open and parse the blend-file at a high level, but only handles its basic data layout "
|
||||
"(usable when the given files are not valid blend-files - e.g. corrupted ones)"))
|
||||
|
||||
group = parser.add_argument_group("Filters", FILTER_DOC)
|
||||
group.add_argument(
|
||||
"--filter-block", dest="block_filters", nargs=3, action='append',
|
||||
help=("Filter to apply to BLOCKS (a.k.a. data itself)"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
if not args.output:
|
||||
if args.check_file:
|
||||
args.output = [None] * len(args.input)
|
||||
else:
|
||||
args.output = [os.path.splitext(infile)[0] + ".json" for infile in args.input]
|
||||
|
||||
if args.block_filters:
|
||||
args.block_filters = [(True if m[0] == "+" else False,
|
||||
0 if len(m) == 1 else (-1 if m[1] == "*" else int(m[1:])),
|
||||
re.compile(f), re.compile(d))
|
||||
for m, f, d in args.block_filters]
|
||||
|
||||
if args.filter_data:
|
||||
if args.full_data:
|
||||
args.filter_data = None
|
||||
else:
|
||||
args.filter_data = {n.encode() for n in args.filter_data.split(',')}
|
||||
|
||||
for infile, outfile in zip(args.input, args.output):
|
||||
if args.raw_bblock:
|
||||
with blendfile.open_blend(infile, wrapper_type=blendfile.BlendFileRaw) as blend:
|
||||
address_map = gen_fake_addresses(args, blend)
|
||||
|
||||
if outfile:
|
||||
with open(outfile, 'w', encoding="ascii", errors='xmlcharrefreplace') as f:
|
||||
blend_to_json(args, f, blend, address_map)
|
||||
continue
|
||||
|
||||
with blendfile.open_blend(infile) as blend:
|
||||
address_map = gen_fake_addresses(args, blend)
|
||||
|
||||
if args.check_file:
|
||||
check_file(args, blend)
|
||||
|
||||
if outfile:
|
||||
with open(outfile, 'w', encoding="ascii", errors='xmlcharrefreplace') as f:
|
||||
blend_to_json(args, f, blend, address_map)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
203
blender-5.2.0/tools/utils/blender_keyconfig_export_permutations.py
Executable file
203
blender-5.2.0/tools/utils/blender_keyconfig_export_permutations.py
Executable file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
r"""
|
||||
This script exports permutations of key-maps with different settings modified.
|
||||
|
||||
Useful for checking changes intended for one configuration don't impact others accidentally.
|
||||
|
||||
./blender.bin -b --factory-startup \
|
||||
--python tools/utils/blender_keyconfig_export_permutations.py -- \
|
||||
--preset=Blender \
|
||||
--output-dir=./output \
|
||||
--keymap-prefs=select_mouse:rmb_action
|
||||
|
||||
/blender.bin -b --factory-startup \
|
||||
--python tools/utils/blender_keyconfig_export_permutations.py -- \
|
||||
--preset=Blender_27x \
|
||||
--output-dir=output \
|
||||
--keymap-prefs="select_mouse"
|
||||
|
||||
The preferences setting: ``select_mouse:rmb_action`` expands into:
|
||||
|
||||
config = [
|
||||
("select_mouse", ('LEFT', 'RIGHT')),
|
||||
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
|
||||
]
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--preset",
|
||||
dest="preset",
|
||||
default="Blender",
|
||||
metavar='PRESET', type=str,
|
||||
help="The name of the preset to export",
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
dest="output_dir",
|
||||
default=".",
|
||||
metavar='OUTPUT_DIR', type=str,
|
||||
help="The directory to output to.",
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--keymap-prefs",
|
||||
dest="keymap_prefs",
|
||||
default="select_mouse:rmb_action",
|
||||
metavar='KEYMAP_PREFS', type=str,
|
||||
help=(
|
||||
"Colon separated list of attributes to generate key-map configuration permutations. "
|
||||
"WARNING: as all combinations are tested, their number increases exponentially!"
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def permutations_from_attrs_impl(config, permutation, index):
|
||||
index_next = index + 1
|
||||
attr, values = config[index]
|
||||
for val in values:
|
||||
permutation[index] = (attr, val)
|
||||
if index_next == len(config):
|
||||
yield tuple(permutation)
|
||||
else:
|
||||
# Keep walking down the list of permutations.
|
||||
yield from permutations_from_attrs_impl(config, permutation, index_next)
|
||||
# Not necessary, just ensure stale values aren't used.
|
||||
permutation[index] = None
|
||||
|
||||
|
||||
def permutations_from_attrs(config):
|
||||
"""
|
||||
Take a list of attributes and possible values:
|
||||
|
||||
config = [
|
||||
("select_mouse", ('LEFT', 'RIGHT')),
|
||||
("rmb_action", ('TWEAK', 'FALLBACK_TOOL')),
|
||||
]
|
||||
|
||||
Yielding all permutations:
|
||||
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')],
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'FALLBACK_TOOL')],
|
||||
... etc ...
|
||||
"""
|
||||
if not config:
|
||||
return ()
|
||||
permutation = [None] * len(config)
|
||||
result = list(permutations_from_attrs_impl(config, permutation, 0))
|
||||
assert permutation == ([None] * len(config))
|
||||
return result
|
||||
|
||||
|
||||
def permutation_as_filename(preset, values):
|
||||
"""
|
||||
Takes a configuration, eg:
|
||||
|
||||
[("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')]
|
||||
|
||||
And returns a filename compatible path:
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
if not values:
|
||||
return quote(preset)
|
||||
|
||||
return quote(
|
||||
preset + "_" + ".".join([
|
||||
"-".join((str(key), str(val)))
|
||||
for key, val in values
|
||||
]),
|
||||
# Needed so forward slashes aren't included in the resulting name.
|
||||
safe="",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
try:
|
||||
import bpy
|
||||
except ImportError:
|
||||
# Run outside of Blender, just show "--help".
|
||||
bpy = None
|
||||
args.insert(0, "--help")
|
||||
|
||||
args = argparse_create().parse_args(args)
|
||||
if bpy is None:
|
||||
return
|
||||
|
||||
from bpy import context
|
||||
|
||||
preset = args.preset
|
||||
output_dir = args.output_dir
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Needed for background mode.
|
||||
preset_filepath = bpy.utils.preset_find(preset, preset_path="keyconfig")
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
|
||||
|
||||
# Key-map preferences..
|
||||
km_prefs = context.window_manager.keyconfigs.active.preferences
|
||||
config = []
|
||||
# Use RNA introspection:
|
||||
if args.keymap_prefs:
|
||||
for attr in args.keymap_prefs.split(":"):
|
||||
if not hasattr(km_prefs, attr):
|
||||
print(f"KeyMap preferences does not have attribute: {attr:s}")
|
||||
sys.exit(1)
|
||||
|
||||
prop_def = km_prefs.rna_type.properties.get(attr)
|
||||
match prop_def.type:
|
||||
case 'ENUM':
|
||||
value = tuple(val.identifier for val in prop_def.enum_items)
|
||||
case 'BOOLEAN':
|
||||
value = (True, False)
|
||||
case _ as prop_def_type:
|
||||
raise Exception(f"Unhandled attribute type {prop_def_type:s}")
|
||||
config.append((attr, value))
|
||||
config = tuple(config)
|
||||
|
||||
for attr_permutation in (permutations_from_attrs(config) or ((),)):
|
||||
|
||||
# Reload and set.
|
||||
if attr_permutation is not None:
|
||||
km_prefs = context.window_manager.keyconfigs.active.preferences
|
||||
for attr, value in attr_permutation:
|
||||
setattr(km_prefs, attr, value)
|
||||
# Re-activate after setting preferences, tsk, ideally this shouldn't be necessary.
|
||||
bpy.ops.preferences.keyconfig_activate(filepath=preset_filepath)
|
||||
|
||||
filepath = os.path.join(output_dir, permutation_as_filename(preset, attr_permutation) + ".py")
|
||||
|
||||
print("Writing:", filepath)
|
||||
bpy.ops.preferences.keyconfig_export(filepath=filepath, all=True)
|
||||
|
||||
sys.exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
127
blender-5.2.0/tools/utils/blender_merge_format_changes.py
Executable file
127
blender-5.2.0/tools/utils/blender_merge_format_changes.py
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# We unfortunately ended with three commits instead of a single one to be handled as
|
||||
# 'clang-format' commit, we are handling them as a single 'block'.
|
||||
format_commits = (
|
||||
'e12c08e8d170b7ca40f204a5b0423c23a9fbc2c1',
|
||||
'91a9cd0a94000047248598394c41ac30f893f147',
|
||||
'3076d95ba441cd32706a27d18922a30f8fd28b8a',
|
||||
)
|
||||
pre_format_commit = format_commits[0] + '~1'
|
||||
|
||||
|
||||
def get_string(cmd):
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode('utf8').strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Parse arguments.
|
||||
mode = None
|
||||
base_branch = 'main'
|
||||
if len(sys.argv) >= 2:
|
||||
# Note that recursive conflict resolution strategy has to reversed in rebase compared to merge.
|
||||
# See https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt--m
|
||||
if sys.argv[1] == '--rebase':
|
||||
mode = 'rebase'
|
||||
recursive_format_commit_merge_options = '-Xignore-all-space -Xtheirs'
|
||||
elif sys.argv[1] == '--merge':
|
||||
mode = 'merge'
|
||||
recursive_format_commit_merge_options = '-Xignore-all-space -Xours'
|
||||
if len(sys.argv) == 4:
|
||||
if sys.argv[2] == '--base_branch':
|
||||
base_branch = sys.argv[3]
|
||||
|
||||
if mode is None:
|
||||
print("Merge or rebase Blender main (or another base branch) into a branch in 3 steps,")
|
||||
print("to automatically merge clang-format changes.")
|
||||
print("")
|
||||
print(" --rebase Perform equivalent of 'git rebase main'")
|
||||
print(" --merge Perform equivalent of 'git merge main'")
|
||||
print("")
|
||||
print("Optional arguments:")
|
||||
print(" --base_branch <branch name> Use given branch instead of main")
|
||||
print(" (assuming that base branch has already been updated")
|
||||
print(" and has the initial clang-format commit).")
|
||||
return 0
|
||||
|
||||
# Verify we are in the right directory.
|
||||
root_path = get_string(['git', 'rev-parse', '--show-superproject-working-tree'])
|
||||
if os.path.realpath(root_path) != os.path.realpath(os.getcwd()):
|
||||
print("BLENDER MERGE: must run from blender repository root directory")
|
||||
return 1
|
||||
|
||||
# Abort if a rebase is still progress.
|
||||
rebase_merge = get_string(['git', 'rev-parse', '--git-path', 'rebase-merge'])
|
||||
rebase_apply = get_string(['git', 'rev-parse', '--git-path', 'rebase-apply'])
|
||||
merge_head = get_string(['git', 'rev-parse', '--git-path', 'MERGE_HEAD'])
|
||||
if (
|
||||
os.path.exists(rebase_merge) or
|
||||
os.path.exists(rebase_apply) or
|
||||
os.path.exists(merge_head)
|
||||
):
|
||||
print("BLENDER MERGE: rebase or merge in progress, complete it first")
|
||||
return 1
|
||||
|
||||
# Abort if uncommitted changes.
|
||||
changes = get_string(['git', 'status', '--porcelain', '--untracked-files=no'])
|
||||
if len(changes) != 0:
|
||||
print("BLENDER MERGE: detected uncommitted changes, cannot run")
|
||||
return 1
|
||||
|
||||
# Setup command, with commit message for merge commits.
|
||||
if mode == 'rebase':
|
||||
mode_cmd = 'rebase'
|
||||
else:
|
||||
branch = get_string(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
|
||||
mode_cmd = 'merge --no-edit -m "Merge \'' + base_branch + '\' into \'' + branch + '\'"'
|
||||
|
||||
# Rebase up to the clang-format commit.
|
||||
code = os.system('git merge-base --is-ancestor ' + pre_format_commit + ' HEAD')
|
||||
if code != 0:
|
||||
code = os.system('git ' + mode_cmd + ' ' + pre_format_commit)
|
||||
if code != 0:
|
||||
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and run again")
|
||||
return code
|
||||
|
||||
# Rebase clang-format commit.
|
||||
code = os.system('git merge-base --is-ancestor ' + format_commits[-1] + ' HEAD')
|
||||
if code != 0:
|
||||
os.system('git ' + mode_cmd + ' ' + recursive_format_commit_merge_options + ' ' + format_commits[-1])
|
||||
paths = get_string(('git', '--no-pager', 'diff', '--name-only', format_commits[-1])).replace('\n', ' ')
|
||||
if sys.platform == 'win32' and len(paths) > 8000:
|
||||
# Windows command-line does not accept more than 8191 chars.
|
||||
os.system('make format')
|
||||
else:
|
||||
os.system('make format PATHS="' + paths + '"')
|
||||
os.system('git add -u')
|
||||
count = int(get_string(['git', 'rev-list', '--count', '' + format_commits[-1] + '..HEAD']))
|
||||
if count == 1 or mode == 'merge':
|
||||
# Amend if we just have a single commit or are merging.
|
||||
os.system('git commit --amend --no-edit')
|
||||
else:
|
||||
# Otherwise create a commit for formatting.
|
||||
os.system('git commit -m "Cleanup: apply clang format"')
|
||||
|
||||
# Rebase remaining commits
|
||||
code = os.system('git ' + mode_cmd + ' ' + base_branch)
|
||||
if code != 0:
|
||||
print("BLENDER MERGE: resolve conflicts, complete " + mode + " and you're done")
|
||||
else:
|
||||
print("BLENDER MERGE: done")
|
||||
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
292
blender-5.2.0/tools/utils/blender_theme_as_c.py
Executable file
292
blender-5.2.0/tools/utils/blender_theme_as_c.py
Executable file
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Generates 'userdef_default_theme.c' from a 'userpref.blend' file.
|
||||
|
||||
Pass your user preferences blend file to this script to update the C source file.
|
||||
|
||||
eg:
|
||||
|
||||
./tools/utils/blender_theme_as_c.py ~/.config/blender/2.80/config/userpref.blend
|
||||
|
||||
.. or find the latest:
|
||||
|
||||
./tools/utils/blender_theme_as_c.py $(find ~/.config/blender -name "userpref.blend" | sort | tail -1)
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
C_SOURCE_HEADER = r'''/* SPDX-FileCopyrightText: 2018 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/**
|
||||
* Generated by 'tools/utils/blender_theme_as_c.py'
|
||||
*
|
||||
* Do not hand edit this file!
|
||||
*/
|
||||
|
||||
#include "DNA_theme_types.h"
|
||||
|
||||
/* clang-format off */
|
||||
|
||||
/* NOTE: this is endianness-sensitive. */
|
||||
#define RGBA(c) {((c) >> 24) & 0xff, ((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
||||
#define RGB(c) {((c) >> 16) & 0xff, ((c) >> 8) & 0xff, (c) & 0xff}
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def round_float_32(f):
|
||||
from struct import pack, unpack
|
||||
return unpack("f", pack("f", f))[0]
|
||||
|
||||
|
||||
def repr_f32(f):
|
||||
f_round = round_float_32(f)
|
||||
f_str = repr(f)
|
||||
f_str_frac = f_str.partition(".")[2]
|
||||
if not f_str_frac:
|
||||
return f_str
|
||||
for i in range(1, len(f_str_frac)):
|
||||
f_test = round(f, i)
|
||||
f_test_round = round_float_32(f_test)
|
||||
if f_test_round == f_round:
|
||||
return "{:.{:d}f}".format(f_test, i)
|
||||
return f_str
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# Avoid maintaining multiple blendfile modules
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "modules"))
|
||||
del sys
|
||||
|
||||
source_dst = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..",
|
||||
"release", "datafiles", "userdef", "userdef_default_theme.c",
|
||||
)
|
||||
|
||||
dna_rename_defs_h = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "..",
|
||||
"source", "blender", "makesdna", "intern", "dna_rename_defs.h",
|
||||
)
|
||||
|
||||
|
||||
def dna_rename_defs(blend):
|
||||
"""
|
||||
"""
|
||||
from blendfile import DNAName
|
||||
import re
|
||||
re_dna_struct_rename = re.compile(
|
||||
r'DNA_STRUCT_RENAME+\('
|
||||
r'([a-zA-Z0-9_]+)' r',\s*'
|
||||
r'([a-zA-Z0-9_]+)' r'\)',
|
||||
)
|
||||
|
||||
re_dna_struct_rename_elem = re.compile(
|
||||
r'DNA_STRUCT_RENAME_MEMBER+\('
|
||||
r'([a-zA-Z0-9_]+)' r',\s*'
|
||||
r'([a-zA-Z0-9_]+)' r',\s*'
|
||||
r'([a-zA-Z0-9_]+)' r'\)',
|
||||
)
|
||||
with open(dna_rename_defs_h, 'r', encoding='utf-8') as fh:
|
||||
data = fh.read()
|
||||
|
||||
struct_runtime_to_storage_map = {}
|
||||
member_runtime_to_storage_map = {}
|
||||
|
||||
for line in data.split('\n'):
|
||||
m = re_dna_struct_rename.match(line)
|
||||
if m is not None:
|
||||
struct_storage, struct_runtime = m.groups()
|
||||
struct_runtime_to_storage_map[struct_runtime] = struct_storage
|
||||
continue
|
||||
|
||||
m = re_dna_struct_rename_elem.match(line)
|
||||
if m is not None:
|
||||
struct_name_runtime, member_storage, member_runtime = m.groups()
|
||||
if struct_name_runtime not in member_runtime_to_storage_map:
|
||||
member_runtime_to_storage_map[struct_name_runtime] = []
|
||||
member_runtime_to_storage_map[struct_name_runtime].append((member_storage, member_runtime))
|
||||
continue
|
||||
|
||||
for struct_name_runtime, members in member_runtime_to_storage_map.items():
|
||||
if len(members) > 1:
|
||||
# Order renames that are themselves destinations to go first, so that the item is not removed.
|
||||
# Needed e.g.
|
||||
# `DNA_STRUCT_RENAME_ELEM(Light, energy_new, energy);`
|
||||
# `DNA_STRUCT_RENAME_ELEM(Light, energy, energy_deprecated)`
|
||||
# ... in this case the order matters.
|
||||
member_runtime_set = set(member_runtime for (_member_storage, member_runtime) in members)
|
||||
members_ordered = ([], [])
|
||||
for (member_storage, member_runtime) in members:
|
||||
members_ordered[member_storage not in member_runtime_set].append((member_storage, member_runtime))
|
||||
members = members_ordered[0] + members_ordered[1]
|
||||
del member_runtime_set, members_ordered
|
||||
|
||||
for (member_storage, member_runtime) in members:
|
||||
struct_name_storage = struct_runtime_to_storage_map.get(struct_name_runtime, struct_name_runtime)
|
||||
struct_name_storage = struct_name_storage.encode('utf-8')
|
||||
# The struct itself may have been renamed.
|
||||
member_storage = member_storage.encode('utf-8')
|
||||
member_runtime = member_runtime.encode('utf-8')
|
||||
dna_struct = blend.structs[blend.sdna_index_from_id[struct_name_storage]]
|
||||
for field in dna_struct.fields:
|
||||
dna_name = field.dna_name
|
||||
if member_storage == dna_name.name_only:
|
||||
field.dna_name = dna_name = DNAName(dna_name.name_full)
|
||||
del dna_struct.field_from_name[dna_name.name_only]
|
||||
dna_name.name_full = dna_name.name_full.replace(member_storage, member_runtime)
|
||||
dna_name.name_only = member_runtime
|
||||
dna_struct.field_from_name[dna_name.name_only] = field
|
||||
|
||||
|
||||
def theme_data(userpref_filename):
|
||||
import blendfile
|
||||
blend = blendfile.open_blend(userpref_filename)
|
||||
dna_rename_defs(blend)
|
||||
u = next((c for c in blend.blocks if c.code == b'USER'), None)
|
||||
# theme_type = b.sdna_index_from_id[b'bTheme']
|
||||
t = u.get_pointer((b'themes', b'first'))
|
||||
t.refine_type(b'bTheme')
|
||||
return blend, t
|
||||
|
||||
|
||||
def is_ignore_dna_name(name):
|
||||
if name.startswith(b'_'):
|
||||
return True
|
||||
elif name in {
|
||||
b'active_theme_area',
|
||||
}:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def write_member(fw, indent, _blend, _theme, ls):
|
||||
path_old = ()
|
||||
|
||||
for key, value in ls:
|
||||
key = key if type(key) is tuple else (key,)
|
||||
path_new = key[:-1]
|
||||
|
||||
if tuple(path_new) != tuple(path_old):
|
||||
if path_old:
|
||||
p = len(path_old) - 1
|
||||
while p >= 0 and (p >= len(path_new) or path_new[p] != path_old[p]):
|
||||
indent = p + 1
|
||||
fw('\t' * indent)
|
||||
fw('},\n')
|
||||
p -= 1
|
||||
del p
|
||||
|
||||
p = 0
|
||||
for p in range(min(len(path_old), len(path_new))):
|
||||
if path_old[p] != key[p]:
|
||||
break
|
||||
else:
|
||||
p = p + 1
|
||||
|
||||
for i, c in enumerate(path_new[p:]):
|
||||
indent = p + i + 1
|
||||
fw('\t' * indent)
|
||||
if type(c) is bytes:
|
||||
attr = c.decode('ascii')
|
||||
fw(f'.{attr} = ')
|
||||
fw('{\n')
|
||||
|
||||
if not is_ignore_dna_name(key[-1]):
|
||||
indent = '\t' * (len(path_new) + 1)
|
||||
attr = key[-1].decode('ascii')
|
||||
if isinstance(value, float):
|
||||
if value != 0.0:
|
||||
value_repr = repr_f32(value)
|
||||
fw(f'{indent}.{attr} = {value_repr}f,\n')
|
||||
elif isinstance(value, int):
|
||||
if value != 0:
|
||||
fw(f'{indent}.{attr} = {value},\n')
|
||||
elif isinstance(value, bytes):
|
||||
if set(value) != {0}:
|
||||
if len(value) == 3:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = RGB(0x{value_repr}),\n')
|
||||
elif len(value) == 4:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = RGBA(0x{value_repr}),\n')
|
||||
else:
|
||||
value = value.rstrip(b'\x00')
|
||||
is_ascii = True
|
||||
for ub in value:
|
||||
if not (ub >= 32 and ub < 127):
|
||||
is_ascii = False
|
||||
break
|
||||
if is_ascii:
|
||||
value_repr = value.decode('ascii')
|
||||
fw(f'{indent}.{attr} = "{value_repr}",\n')
|
||||
else:
|
||||
value_repr = "".join(f'{ub:02x}' for ub in value)
|
||||
fw(f'{indent}.{attr} = {{{value_repr}}},\n')
|
||||
else:
|
||||
fw(f'{indent}.{attr} = {value},\n')
|
||||
path_old = path_new
|
||||
|
||||
|
||||
def convert_data(blend, theme, f):
|
||||
fw = f.write
|
||||
fw(C_SOURCE_HEADER)
|
||||
fw('const bTheme U_theme_default = {\n')
|
||||
ls = list(theme.items_recursive_iter(use_nil=False))
|
||||
write_member(fw, 1, blend, theme, ls)
|
||||
|
||||
fw('};\n')
|
||||
fw('\n')
|
||||
fw('/* clang-format on */\n')
|
||||
|
||||
|
||||
def file_remove_empty_braces(source_dst):
|
||||
with open(source_dst, 'r', encoding='utf-8') as fh:
|
||||
data = fh.read()
|
||||
# Remove:
|
||||
# .foo = { }
|
||||
import re
|
||||
|
||||
def key_replace(match):
|
||||
del match
|
||||
return ""
|
||||
data_prev = None
|
||||
# Braces may become empty by removing nested
|
||||
while data != data_prev:
|
||||
data_prev = data
|
||||
data = re.sub(
|
||||
r'\s+\.[a-zA-Z_0-9]+\s+=\s+\{\s*\},',
|
||||
key_replace, data, re.MULTILINE,
|
||||
)
|
||||
|
||||
# Use two spaces instead of tabs.
|
||||
data = data.replace('\t', ' ')
|
||||
|
||||
with open(source_dst, 'w', encoding='utf-8') as fh:
|
||||
fh.write(data)
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
blend, theme = theme_data(sys.argv[-1])
|
||||
with open(source_dst, 'w', encoding='utf-8') as fh:
|
||||
convert_data(blend, theme, fh)
|
||||
|
||||
# Microsoft Visual Studio doesn't support empty braces.
|
||||
file_remove_empty_braces(source_dst)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
441
blender-5.2.0/tools/utils/credits_git_gen.py
Executable file
441
blender-5.2.0/tools/utils/credits_git_gen.py
Executable file
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
Example use to generate all credits:
|
||||
|
||||
credits_git_gen.py
|
||||
|
||||
Example use a custom range:
|
||||
|
||||
credits_git_gen.py --source=/src/blender --range=SHA1..HEAD
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
# NOTE: this shares the basic structure with `credits_git_gen.py`,
|
||||
# however details differ enough for them to be separate scripts.
|
||||
# Improvements to this script may apply there too.
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
from collections.abc import (
|
||||
Iterable,
|
||||
)
|
||||
|
||||
from git_log import (
|
||||
GitCommitIter,
|
||||
GitCommit,
|
||||
)
|
||||
|
||||
import git_data_canonical_authors
|
||||
import git_data_sha1_override_authors
|
||||
|
||||
IS_ATTY = sys.stdout.isatty()
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
SOURCE_DIR = os.path.normpath(os.path.join(BASE_DIR, "..", ".."))
|
||||
|
||||
author_table = git_data_canonical_authors.canonical_author_map()
|
||||
author_override_table = git_data_sha1_override_authors.sha1_authors_map()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Multi-Processing
|
||||
|
||||
def process_commits_for_map(commits: Iterable[GitCommit]) -> "Credits":
|
||||
result = Credits()
|
||||
for c in commits:
|
||||
result.process_commit(c)
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Progress Display
|
||||
|
||||
def value_as_percentage(value_partial: int, value_final: int) -> str:
|
||||
percent = 0.0 if (value_final == 0) else (value_partial / value_final)
|
||||
return "{:-6.2f}%".format(percent * 100)
|
||||
|
||||
|
||||
if IS_ATTY:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("\r\033[K[{:s}]: {:s}".format(value_as_percentage(value_partial, value_final), info))
|
||||
else:
|
||||
def progress_output(value_partial: int, value_final: int, info: str) -> None:
|
||||
sys.stdout.write("[{:s}]: {:s}\n".format(value_as_percentage(value_partial, value_final), info))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Class for generating credits
|
||||
|
||||
class CreditUser:
|
||||
__slots__ = (
|
||||
"commit_total",
|
||||
"year_min",
|
||||
"year_max",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.commit_total = 0
|
||||
self.year_min = 0
|
||||
self.year_max = 0
|
||||
|
||||
|
||||
class Credits:
|
||||
__slots__ = (
|
||||
"users",
|
||||
# Use for progress, simply the number of times `process_commit` has been called.
|
||||
"process_commits_count",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.users: dict[str, CreditUser] = {}
|
||||
self.process_commits_count = 0
|
||||
|
||||
@classmethod
|
||||
def commit_authors_get(cls, c: GitCommit) -> list[str]:
|
||||
if (authors_overwrite := author_override_table.get(c.sha1, None)) is not None:
|
||||
# Ignore git commit info for these having an entry in `author_override_table`.
|
||||
return [author_table.get(author, author) for author in authors_overwrite]
|
||||
|
||||
authors = [c.author] + c.co_authors
|
||||
# Normalize author string into canonical form, prevents duplicate credit users
|
||||
authors = [unicodedata.normalize('NFC', author) for author in authors]
|
||||
return [author_table.get(author, author) for author in authors]
|
||||
|
||||
@classmethod
|
||||
def is_credit_commit_valid(cls, c: GitCommit) -> bool:
|
||||
ignore_dir = (
|
||||
b"blender/extern/",
|
||||
b"blender/intern/opennl/",
|
||||
)
|
||||
|
||||
if not any(f for f in c.files if not f.startswith(ignore_dir)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def merge(self, other: "Credits") -> None:
|
||||
"""
|
||||
Merge other Credits into this, clearing the other.
|
||||
"""
|
||||
for user_key, user_other in other.users.items():
|
||||
user = self.users.get(user_key)
|
||||
if user is None:
|
||||
# Consume the user.
|
||||
self.users[user_key] = user_other
|
||||
else:
|
||||
user.commit_total += user_other.commit_total
|
||||
user.year_min = min(user.year_min, user_other.year_min)
|
||||
user.year_max = max(user.year_max, user_other.year_max)
|
||||
other.users.clear()
|
||||
|
||||
def process_commit(self, c: GitCommit) -> None:
|
||||
self.process_commits_count += 1
|
||||
|
||||
if not self.is_credit_commit_valid(c):
|
||||
return
|
||||
|
||||
authors = self.commit_authors_get(c)
|
||||
year = c.date.year
|
||||
for author in authors:
|
||||
cu = self.users.get(author)
|
||||
if cu is None:
|
||||
cu = self.users[author] = CreditUser()
|
||||
cu.year_min = year
|
||||
cu.year_max = year
|
||||
|
||||
cu.commit_total += 1
|
||||
cu.year_min = min(cu.year_min, year)
|
||||
cu.year_max = max(cu.year_max, year)
|
||||
|
||||
def _process_multiprocessing(self, commit_iter: Iterable[GitCommit], *, jobs: int) -> None:
|
||||
print("Collecting commits...")
|
||||
# NOTE(@ideasman42): that the chunk size doesn't have as much impact on
|
||||
# performance as you might expect, values between 16 and 1024 seem reasonable.
|
||||
# Although higher values tend to bottleneck as the process finishes.
|
||||
chunk_size = 256
|
||||
chunk_list = []
|
||||
chunk = []
|
||||
for c in commit_iter:
|
||||
chunk.append(c)
|
||||
if len(chunk) >= chunk_size:
|
||||
chunk_list.append(chunk)
|
||||
chunk = []
|
||||
if chunk:
|
||||
chunk_list.append(chunk)
|
||||
|
||||
commit_count_total = (max(len(chunk_list) - 1, 0) * chunk_size) + len(chunk)
|
||||
|
||||
print("Found {:,d} commits, processing...".format(commit_count_total))
|
||||
commit_count_handled = 0
|
||||
with multiprocessing.Pool(processes=jobs) as pool:
|
||||
for result in pool.imap_unordered(process_commits_for_map, chunk_list):
|
||||
commit_count_handled += result.process_commits_count
|
||||
progress_output(
|
||||
commit_count_handled,
|
||||
commit_count_total,
|
||||
"{:,d} / {:,d} commits".format(commit_count_handled, commit_count_total),
|
||||
)
|
||||
self.merge(result)
|
||||
|
||||
if IS_ATTY:
|
||||
print("") # Was printing on one-line, move to next.
|
||||
|
||||
def process(self, commit_iter: Iterable[GitCommit], *, jobs: int) -> None:
|
||||
if jobs > 1:
|
||||
self._process_multiprocessing(commit_iter, jobs=jobs)
|
||||
return
|
||||
|
||||
# Simple single process operation.
|
||||
for i, c in enumerate(commit_iter):
|
||||
self.process_commit(c)
|
||||
if not (i % 100):
|
||||
print(i)
|
||||
|
||||
def write_object(
|
||||
self,
|
||||
fh: io.TextIOWrapper,
|
||||
*,
|
||||
is_main_credits: bool = True,
|
||||
contrib_companies: tuple[str, ...] = (),
|
||||
sort: str = "name",
|
||||
use_email: bool = False,
|
||||
) -> None:
|
||||
commit_word = "commit", "commits"
|
||||
from unicodedata import normalize
|
||||
# Normalize using decomposed strings for sorting.
|
||||
# That makes letters with diacritics grouped with the undiacritized variant.
|
||||
# Collation is not perfect, but better than having non-English letters at the very bottom of the list.
|
||||
|
||||
if sort == "commit":
|
||||
sorted_authors = dict(sorted(
|
||||
self.users.items(),
|
||||
key=lambda item: (item[1].commit_total, normalize('NFD', item[0]).casefold()),
|
||||
))
|
||||
else:
|
||||
sorted_authors = dict(sorted(
|
||||
self.users.items(),
|
||||
key=lambda item: (normalize('NFD', item[0]).casefold(), item[1].commit_total),
|
||||
))
|
||||
|
||||
fh.write("<h3>Individual Contributors</h3>\n\n")
|
||||
for author, cu in sorted_authors.items():
|
||||
fh.write("{:s}, {:,d} {:s} {:s}<br />\n".format(
|
||||
html.escape(author if use_email else author.split("<", 1)[0].rstrip()),
|
||||
cu.commit_total,
|
||||
commit_word[cu.commit_total > 1],
|
||||
(
|
||||
"" if not is_main_credits else (
|
||||
"- {:d}".format(cu.year_min) if cu.year_min == cu.year_max else
|
||||
"({:d} - {:d})".format(cu.year_min, cu.year_max)
|
||||
)
|
||||
),
|
||||
))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Companies, hard coded
|
||||
if is_main_credits:
|
||||
fh.write("<h3>Contributions from Companies & Organizations</h3>\n")
|
||||
fh.write("<p>\n")
|
||||
for line in contrib_companies:
|
||||
fh.write("{:s}<br />\n".format(line))
|
||||
fh.write("</p>\n")
|
||||
|
||||
import datetime
|
||||
now = datetime.datetime.now()
|
||||
fn = os.path.basename(__file__)
|
||||
fh.write(
|
||||
"<p><center><i>Generated by '{:s}' {:d}/{:02d}/{:02d}</i></center></p>\n".format(
|
||||
fn, now.year, now.month, now.day,
|
||||
))
|
||||
|
||||
def write(
|
||||
self,
|
||||
filepath: str,
|
||||
*,
|
||||
is_main_credits: bool = True,
|
||||
contrib_companies: tuple[str, ...] = (),
|
||||
sort: str = "name",
|
||||
use_email: bool = False,
|
||||
) -> None:
|
||||
with open(filepath, 'w', encoding="ascii", errors='xmlcharrefreplace') as fh:
|
||||
self.write_object(
|
||||
fh,
|
||||
is_main_credits=is_main_credits,
|
||||
contrib_companies=contrib_companies,
|
||||
sort=sort,
|
||||
use_email=use_email,
|
||||
)
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
|
||||
# When `--help` or no arguments are given, print this help.
|
||||
usage_text = "Review revisions."
|
||||
|
||||
epilog = "This script is used to generate credits"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
||||
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
dest="source_dir",
|
||||
metavar='PATH',
|
||||
required=False,
|
||||
default=SOURCE_DIR,
|
||||
help="Path to git repository",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--range",
|
||||
dest="range_sha1",
|
||||
metavar='SHA1_RANGE',
|
||||
required=False,
|
||||
default="HEAD",
|
||||
help="Range to use, eg: 169c95b8..HEAD or HEAD for all history.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--sort",
|
||||
dest="sort",
|
||||
metavar='METHOD',
|
||||
required=False,
|
||||
help="Sort credits by 'name' (default) or 'commit'",
|
||||
)
|
||||
# Don't include email addresses, they're useful for identifying developers but better not include in credits.
|
||||
# Even though it's publicly available, developers may not want this so easily accessible on the credits page.
|
||||
parser.add_argument(
|
||||
"--use-email",
|
||||
dest="use_email",
|
||||
required=False,
|
||||
action='store_true',
|
||||
help="Include the email address in the credits (useful for debugging/investigating issues)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
dest="jobs",
|
||||
type=int,
|
||||
default=0,
|
||||
help=(
|
||||
"The number of processes to use. "
|
||||
"Defaults to zero which detects the available cores, 1 is single threaded (useful for debugging)."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def parse_organizations(authors_file: str) -> tuple[str, ...] | None:
|
||||
"""
|
||||
Parse the organizations section from the AUTHORS file.
|
||||
Return the sorted organizations or None if they could not be found.
|
||||
"""
|
||||
orgs = []
|
||||
section_begin = "# BEGIN organizations section"
|
||||
section_end = "# END organizations section"
|
||||
in_section = False
|
||||
found_end = False
|
||||
|
||||
with open(authors_file, "r", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
if line.startswith(section_begin):
|
||||
in_section = True
|
||||
continue
|
||||
if line.startswith(section_end):
|
||||
found_end = True
|
||||
break
|
||||
|
||||
if in_section and line and not line.startswith("#"):
|
||||
# Extract organization name before `<`.
|
||||
org_name = line.split("<")[0].strip()
|
||||
# Expected result: `<b>Blender Foundation</b>`.
|
||||
orgs.append("<b>{:s}</b>".format(html.escape(org_name)))
|
||||
|
||||
if not in_section:
|
||||
print("Error: Could not find \"{:s}\" in AUTHORS file: {:s}".format(section_begin, authors_file))
|
||||
return None
|
||||
if not found_end:
|
||||
print("Error: Could not find \"{:s}\" in AUTHORS file: {:s}".format(section_end, authors_file))
|
||||
return None
|
||||
|
||||
sorted_unique_organizations = tuple(sorted(set(orgs), key=str.casefold))
|
||||
return sorted_unique_organizations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
# Get organizations in HTML `<b>` tags.
|
||||
authors_file = os.path.join(args.source_dir, "AUTHORS")
|
||||
contrib_companies = parse_organizations(authors_file)
|
||||
if contrib_companies is None:
|
||||
return 1
|
||||
|
||||
credits = Credits()
|
||||
# commit_range = "HEAD~10..HEAD"
|
||||
# commit_range = "blender-v2.81-release..blender-v2.82-release"
|
||||
# commit_range = "blender-v2.82-release"
|
||||
commit_range = args.range_sha1
|
||||
sort = args.sort
|
||||
jobs = args.jobs
|
||||
if jobs <= 0:
|
||||
# Clamp the value, higher values give errors with too many open files.
|
||||
# Allow users to manually pass very high values in as they might want to tweak system limits themselves.
|
||||
jobs = min(multiprocessing.cpu_count(), 400)
|
||||
|
||||
credits.process(GitCommitIter(args.source_dir, commit_range), jobs=jobs)
|
||||
|
||||
# Report when a single name has multiple entries,
|
||||
# as they may need deduplicating in git_data_canonical_authors.py.
|
||||
unique_author_names = {}
|
||||
unique_author_emails = {}
|
||||
for author in credits.users:
|
||||
author_name, author_email = author.rsplit("<", 1)
|
||||
author_name = author_name.rstrip()
|
||||
author_email = "<" + author_email
|
||||
unique_author_name = unique_author_names.setdefault(author_name, [])
|
||||
unique_author_name.append(author_email)
|
||||
|
||||
unique_author_email = unique_author_emails.setdefault(author_email, [])
|
||||
unique_author_email.append(author_name)
|
||||
for name, emails in sorted(unique_author_names.items()):
|
||||
if len(emails) > 1:
|
||||
print(f"More than one entry for {name}:")
|
||||
for email in sorted(emails):
|
||||
print(f' "{name} {email}",')
|
||||
for email, names in sorted(unique_author_emails.items()):
|
||||
if len(names) > 1:
|
||||
print(f"More than one entry for {email}:")
|
||||
for name in sorted(names):
|
||||
print(f' "{name} {email}",')
|
||||
|
||||
credits.write(
|
||||
"credits.html",
|
||||
is_main_credits=True,
|
||||
contrib_companies=contrib_companies,
|
||||
sort=sort,
|
||||
use_email=args.use_email,
|
||||
)
|
||||
print("Written: credits.html")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
229
blender-5.2.0/tools/utils/cycles_timeit.py
Executable file
229
blender-5.2.0/tools/utils/cycles_timeit.py
Executable file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class COLORS:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
#########################################
|
||||
# Generic helper functions.
|
||||
|
||||
|
||||
def logVerbose(*args):
|
||||
if VERBOSE:
|
||||
print(*args)
|
||||
|
||||
|
||||
def logHeader(*args):
|
||||
print(COLORS.HEADER + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def logWarning(*args):
|
||||
print(COLORS.WARNING + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def logOk(*args):
|
||||
print(COLORS.OKGREEN + COLORS.BOLD, end="")
|
||||
print(*args, end="")
|
||||
print(COLORS.ENDC)
|
||||
|
||||
|
||||
def progress(count, total, prefix="", suffix=""):
|
||||
if VERBOSE:
|
||||
return
|
||||
|
||||
size = shutil.get_terminal_size((80, 20))
|
||||
|
||||
if prefix != "":
|
||||
prefix = prefix + " "
|
||||
if suffix != "":
|
||||
suffix = " " + suffix
|
||||
|
||||
bar_len = size.columns - len(prefix) - len(suffix) - 10
|
||||
filled_len = int(round(bar_len * count / float(total)))
|
||||
|
||||
percents = round(100.0 * count / float(total), 1)
|
||||
bar = '=' * filled_len + '-' * (bar_len - filled_len)
|
||||
|
||||
sys.stdout.write('%s[%s] %s%%%s\r' % (prefix, bar, percents, suffix))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def progressClear():
|
||||
if VERBOSE:
|
||||
return
|
||||
|
||||
size = shutil.get_terminal_size((80, 20))
|
||||
sys.stdout.write(" " * size.columns + "\r")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def humanReadableTimeDifference(seconds):
|
||||
hours = int(seconds) // 60 // 60
|
||||
seconds = seconds - hours * 60 * 60
|
||||
minutes = int(seconds) // 60
|
||||
seconds = seconds - minutes * 60
|
||||
if hours == 0:
|
||||
return "%02d:%05.2f" % (minutes, seconds)
|
||||
else:
|
||||
return "%02d:%02d:%05.2f" % (hours, minutes, seconds)
|
||||
|
||||
|
||||
def humanReadableTimeToSeconds(time):
|
||||
tokens = time.split(".")
|
||||
result = 0
|
||||
if len(tokens) == 2:
|
||||
result = float("0." + tokens[1])
|
||||
mult = 1
|
||||
for token in reversed(tokens[0].split(":")):
|
||||
result += int(token) * mult
|
||||
mult *= 60
|
||||
return result
|
||||
|
||||
#########################################
|
||||
# Benchmark specific helper functions.
|
||||
|
||||
|
||||
def configureArgumentParser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Cycles benchmark helper script.")
|
||||
parser.add_argument("-b", "--binary",
|
||||
help="Full file path to Blender's binary " +
|
||||
"to use for rendering",
|
||||
default="blender")
|
||||
parser.add_argument("-f", "--files", nargs='+')
|
||||
parser.add_argument("-v", "--verbose",
|
||||
help="Perform fully verbose communication",
|
||||
action="store_true",
|
||||
default=False)
|
||||
return parser
|
||||
|
||||
|
||||
def benchmarkFile(blender, blendfile, stats):
|
||||
logHeader("Begin benchmark of file {}" . format(blendfile))
|
||||
# Prepare some regex for parsing
|
||||
re_path_tracing = re.compile(".*Path Tracing Tile ([0-9]+)/([0-9]+)$")
|
||||
re_total_render_time = re.compile(r".*Total render time: ([0-9]+(\.[0-9]+)?)")
|
||||
re_render_time_no_sync = re.compile(
|
||||
".*Render time \\(without synchronization\\): ([0-9]+(\\.[0-9]+)?)")
|
||||
re_pipeline_time = re.compile(r"Time: ([0-9:\.]+) \(Saving: ([0-9:\.]+)\)")
|
||||
# Prepare output folder.
|
||||
# TODO(sergey): Use some proper output folder.
|
||||
output_folder = "/tmp/"
|
||||
# Configure command for the current file.
|
||||
command = (blender,
|
||||
"--background",
|
||||
"--factory-startup",
|
||||
blendfile,
|
||||
"--engine", "CYCLES",
|
||||
"--debug-cycles",
|
||||
"--render-output", output_folder,
|
||||
"--render-format", "PNG",
|
||||
"-f", "1")
|
||||
# Run Blender with configured command line.
|
||||
logVerbose("About to execute command: {}" . format(command))
|
||||
start_time = time.time()
|
||||
process = subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
# Keep reading status while Blender is alive.
|
||||
total_render_time = "N/A"
|
||||
render_time_no_sync = "N/A"
|
||||
pipeline_render_time = "N/A"
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if line == b"" and process.poll() is not None:
|
||||
break
|
||||
line = line.decode().strip()
|
||||
if line == "":
|
||||
continue
|
||||
logVerbose("Line from stdout: {}" . format(line))
|
||||
match = re_path_tracing.match(line)
|
||||
if match:
|
||||
current_tiles = int(match.group(1))
|
||||
total_tiles = int(match.group(2))
|
||||
elapsed_time = time.time() - start_time
|
||||
elapsed_time_str = humanReadableTimeDifference(elapsed_time)
|
||||
progress(current_tiles,
|
||||
total_tiles,
|
||||
prefix="Path Tracing Tiles {}" . format(elapsed_time_str))
|
||||
match = re_total_render_time.match(line)
|
||||
if match:
|
||||
total_render_time = float(match.group(1))
|
||||
match = re_render_time_no_sync.match(line)
|
||||
if match:
|
||||
render_time_no_sync = float(match.group(1))
|
||||
match = re_pipeline_time.match(line)
|
||||
if match:
|
||||
pipeline_render_time = humanReadableTimeToSeconds(match.group(1))
|
||||
|
||||
if process.returncode != 0:
|
||||
return False
|
||||
|
||||
# Clear line used by progress.
|
||||
progressClear()
|
||||
print("Total pipeline render time: {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(pipeline_render_time),
|
||||
pipeline_render_time))
|
||||
print("Total Cycles render time: {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(total_render_time),
|
||||
total_render_time))
|
||||
print("Pure Cycles render time (without sync): {} ({} sec)"
|
||||
. format(humanReadableTimeDifference(render_time_no_sync),
|
||||
render_time_no_sync))
|
||||
logOk("Successfully rendered")
|
||||
stats[blendfile] = {'PIPELINE_TOTAL': pipeline_render_time,
|
||||
'CYCLES_TOTAL': total_render_time,
|
||||
'CYCLES_NO_SYNC': render_time_no_sync}
|
||||
return True
|
||||
|
||||
|
||||
def benchmarkAll(blender, files):
|
||||
stats = {}
|
||||
for blendfile in files:
|
||||
try:
|
||||
benchmarkFile(blender, blendfile, stats)
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
logWarning("Rendering aborted!")
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
parser = configureArgumentParser()
|
||||
args = parser.parse_args()
|
||||
if args.verbose:
|
||||
global VERBOSE
|
||||
VERBOSE = True
|
||||
benchmarkAll(args.binary, args.files)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
66
blender-5.2.0/tools/utils/gdb_struct_repr_c99.py
Normal file
66
blender-5.2.0/tools/utils/gdb_struct_repr_c99.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'''
|
||||
Define the command 'print_struct_c99' for gdb,
|
||||
useful for creating a literal for a nested runtime struct.
|
||||
|
||||
Example use:
|
||||
|
||||
(gdb) source tools/utils/gdb_struct_repr_c99.py
|
||||
(gdb) print_struct_c99 scene->toolsettings
|
||||
'''
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class PrintStructC99(gdb.Command):
|
||||
def __init__(self):
|
||||
super(PrintStructC99, self).__init__(
|
||||
"print_struct_c99",
|
||||
gdb.COMMAND_USER,
|
||||
)
|
||||
|
||||
def get_count_heading(self, string):
|
||||
for i, s in enumerate(string):
|
||||
if s != ' ':
|
||||
break
|
||||
return i
|
||||
|
||||
def extract_typename(self, string):
|
||||
first_line = string.split('\n')[0]
|
||||
return first_line.split('=')[1][:-1].strip()
|
||||
|
||||
def invoke(self, arg, _from_tty):
|
||||
ret_ptype = gdb.execute('ptype {}'.format(arg), to_string=True)
|
||||
tname = self.extract_typename(ret_ptype)
|
||||
print('{} {} = {{'.format(tname, arg))
|
||||
r = gdb.execute('p {}'.format(arg), to_string=True)
|
||||
r = r.split('\n')
|
||||
for rr in r[1:]:
|
||||
if '=' not in rr:
|
||||
print(rr)
|
||||
continue
|
||||
hs = self.get_count_heading(rr)
|
||||
rr_s = rr.strip().split('=', 1)
|
||||
rr_rval = rr_s[1].strip()
|
||||
print(' ' * hs + '.' + rr_s[0] + '= ' + rr_rval)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print('Running GDB from: {:s}\n'.format(gdb.PYTHONDIR))
|
||||
gdb.execute("set print pretty")
|
||||
gdb.execute('set pagination off')
|
||||
gdb.execute('set print repeats 0')
|
||||
gdb.execute('set print elements unlimited')
|
||||
# instantiate
|
||||
PrintStructC99()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
1208
blender-5.2.0/tools/utils/git_data_canonical_authors.py
Normal file
1208
blender-5.2.0/tools/utils/git_data_canonical_authors.py
Normal file
File diff suppressed because it is too large
Load Diff
34
blender-5.2.0/tools/utils/git_data_sha1_override_authors.py
Normal file
34
blender-5.2.0/tools/utils/git_data_sha1_override_authors.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
The intent of this map is to be able to a single canonical author
|
||||
for every ``Name <email>`` combination.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"sha1_authors_map",
|
||||
)
|
||||
|
||||
|
||||
def sha1_authors_map() -> dict[bytes, tuple[str, ...]]:
|
||||
"""
|
||||
Return SHA1 to authors map.
|
||||
"""
|
||||
# Mapping from a commit hash to additional authors.
|
||||
# Fully overwrite authors gathered from git commit info.
|
||||
# Intended usage: Correction of info stored in git commit itself.
|
||||
# Note that the names of the authors here are assumed fully valid and usable as-is.
|
||||
return {
|
||||
# Format: {full_git_hash: (tuple, of, authors),}.
|
||||
# Author was: `blender <blender@localhost.localdomain>`.
|
||||
b"ba3d49225c9ff3514fb87ae5d692baefe5edec30": ("Sergey Sharybin <sergey@blender.org>", ),
|
||||
# Author was: `Author Name <email@address.com>`.
|
||||
b"4b6a4b5bc25bce10367dffadf7718e373f81f299": ("Antonio Vazquez <blendergit@gmail.com>", ),
|
||||
# Author was: `Campbell Barton <campbell@blender.org>`.
|
||||
b"584b96018a575d56e564a239dce0de572bf26c48": ("Lukasz Czyz <lukasz.czyzz@gmail.com>", ),
|
||||
# Author was: `Sergey Sharybin <sergey.vfx@gmail.com>`.
|
||||
b"a76e69f5f75c06dda6d35113d80b6b65dcc94ea0": ("John Cox <johnedwardcox@yahoo.com>", ),
|
||||
}
|
||||
259
blender-5.2.0/tools/utils/git_log.py
Normal file
259
blender-5.2.0/tools/utils/git_log.py
Normal file
@@ -0,0 +1,259 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Simple module for inspecting git commits
|
||||
|
||||
__all__ = (
|
||||
"GitCommit",
|
||||
"GitCommitIter",
|
||||
"GitRepo",
|
||||
)
|
||||
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import datetime
|
||||
import re
|
||||
|
||||
_GIT_COMMIT_COAUTHORS_RE = re.compile(r"^Co-authored-by:[ \t]*([^\n]+)$", re.MULTILINE)
|
||||
|
||||
|
||||
class GitCommit:
|
||||
__slots__ = (
|
||||
"sha1",
|
||||
# to extract more info
|
||||
"_git_dir",
|
||||
|
||||
# cached values
|
||||
"_author",
|
||||
"_date",
|
||||
"_body",
|
||||
"_files",
|
||||
"_files_status",
|
||||
"_diff",
|
||||
)
|
||||
|
||||
def __init__(self, sha1: bytes, git_dir: str):
|
||||
self.sha1 = sha1
|
||||
self._git_dir = git_dir
|
||||
|
||||
self._author: str | None = None
|
||||
self._date: datetime.datetime | None = None
|
||||
self._body: str | None = None
|
||||
self._files: list[bytes] | None = None
|
||||
self._files_status: list[list[bytes]] | None = None
|
||||
self._diff: str | None = None
|
||||
|
||||
def cache(self) -> None:
|
||||
"""
|
||||
Cache all properties
|
||||
(except for diff as it's significantly larger than other members).
|
||||
"""
|
||||
self.author
|
||||
self.date
|
||||
self.body
|
||||
self.files
|
||||
self.files_status
|
||||
|
||||
def _log_format(
|
||||
self,
|
||||
format: str,
|
||||
*,
|
||||
args_prefix: tuple[str | bytes, ...] = (),
|
||||
args_suffix: tuple[str | bytes, ...] = (),
|
||||
) -> bytes:
|
||||
# sha1 = self.sha1.decode('ascii')
|
||||
cmd: tuple[str | bytes, ...] = (
|
||||
"git",
|
||||
*args_prefix,
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"log",
|
||||
"-1", # only this rev
|
||||
self.sha1,
|
||||
"--format=" + format,
|
||||
*args_suffix,
|
||||
)
|
||||
|
||||
# print(" ".join(cmd))
|
||||
|
||||
with subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
) as p:
|
||||
assert p is not None and p.stdout is not None
|
||||
return p.stdout.read()
|
||||
|
||||
@property
|
||||
def sha1_short(self) -> str:
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"rev-parse",
|
||||
"--short",
|
||||
self.sha1,
|
||||
)
|
||||
with subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
) as p:
|
||||
assert p is not None and p.stdout is not None
|
||||
return p.stdout.read().strip().decode('ascii')
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
ret = self._author
|
||||
if ret is None:
|
||||
content = self._log_format("%an <%ae>")[:-1]
|
||||
ret = content.decode("utf8", errors="ignore")
|
||||
self._author = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def co_authors(self) -> list[str]:
|
||||
authors = []
|
||||
for author in _GIT_COMMIT_COAUTHORS_RE.findall(self.body):
|
||||
if not ("<" in author and ">" in author):
|
||||
# Always follow `Name <>` spec, even when no email is given.
|
||||
author = author + " <>"
|
||||
elif not author.endswith(">"):
|
||||
# In case the co-author line has rubbish after the author email.
|
||||
author = author.rsplit(">", 1)[0] + ">"
|
||||
authors.append(author)
|
||||
return authors
|
||||
|
||||
@property
|
||||
def date(self) -> datetime.datetime:
|
||||
ret = self._date
|
||||
if ret is None:
|
||||
import datetime
|
||||
ret = datetime.datetime.fromtimestamp(int(self._log_format("%ct")))
|
||||
self._date = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def body(self) -> str:
|
||||
ret = self._body
|
||||
if ret is None:
|
||||
content = self._log_format("%B")[:-1]
|
||||
ret = content.decode("utf8", errors="ignore")
|
||||
self._body = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def subject(self) -> str:
|
||||
return self.body.lstrip().partition("\n")[0]
|
||||
|
||||
@property
|
||||
def files(self) -> list[bytes]:
|
||||
ret = self._files
|
||||
if ret is None:
|
||||
ret = [
|
||||
f for f in self._log_format(
|
||||
"format:",
|
||||
args_prefix=("-c", "diff.renameLimit=10000"),
|
||||
args_suffix=("--name-only",),
|
||||
).split(b"\n") if f
|
||||
]
|
||||
self._files = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def files_status(self) -> list[list[bytes]]:
|
||||
ret = self._files_status
|
||||
if ret is None:
|
||||
ret = [
|
||||
f.split(None, 1) for f in self._log_format(
|
||||
"format:",
|
||||
args_prefix=("-c", "diff.renameLimit=10000"),
|
||||
args_suffix=("--name-status",),
|
||||
).split(b"\n")
|
||||
if f
|
||||
]
|
||||
self._files_status = ret
|
||||
return ret
|
||||
|
||||
@property
|
||||
def diff(self) -> str:
|
||||
ret = self._diff
|
||||
if ret is None:
|
||||
content = self._log_format(
|
||||
"",
|
||||
args_prefix=("-c", "diff.renameLimit=10000"),
|
||||
args_suffix=("-p",),
|
||||
)
|
||||
ret = content.decode("utf8", errors="ignore")
|
||||
self._diff = ret
|
||||
return ret
|
||||
|
||||
|
||||
class GitCommitIter:
|
||||
__slots__ = (
|
||||
"_path",
|
||||
"_git_dir",
|
||||
"_sha1_range",
|
||||
"_process",
|
||||
)
|
||||
|
||||
def __init__(self, path: str, sha1_range: str):
|
||||
self._path = path
|
||||
self._git_dir = os.path.join(path, ".git")
|
||||
self._sha1_range = sha1_range
|
||||
self._process: subprocess.Popen[bytes] | None = None
|
||||
|
||||
def __iter__(self) -> "GitCommitIter":
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"log",
|
||||
self._sha1_range,
|
||||
"--format=%H",
|
||||
)
|
||||
# print(" ".join(cmd))
|
||||
|
||||
self._process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
return self
|
||||
|
||||
def __next__(self) -> GitCommit:
|
||||
assert self._process is not None and self._process.stdout is not None
|
||||
sha1 = self._process.stdout.readline()[:-1]
|
||||
if sha1:
|
||||
return GitCommit(sha1, self._git_dir)
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
|
||||
class GitRepo:
|
||||
__slots__ = (
|
||||
"_path",
|
||||
"_git_dir",
|
||||
)
|
||||
|
||||
def __init__(self, path: str):
|
||||
self._path = path
|
||||
self._git_dir = os.path.join(path, ".git")
|
||||
|
||||
@property
|
||||
def branch(self) -> bytes:
|
||||
cmd = (
|
||||
"git",
|
||||
"--git-dir",
|
||||
self._git_dir,
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"HEAD",
|
||||
)
|
||||
# print(" ".join(cmd))
|
||||
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
assert p is not None and p.stdout is not None
|
||||
return p.stdout.read()
|
||||
258
blender-5.2.0/tools/utils/git_log_review_commits.py
Executable file
258
blender-5.2.0/tools/utils/git_log_review_commits.py
Executable file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This is a tool for reviewing commit ranges, writing into accept/reject files.
|
||||
|
||||
Useful for reviewing revisions to backport to stable builds.
|
||||
|
||||
Example usage:
|
||||
|
||||
./git_log_review_commits.py --source=../../.. --range=HEAD~40..HEAD --filter=BUGFIX
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
class _Getch:
|
||||
"""
|
||||
Gets a single character from standard input.
|
||||
Does not echo to the screen.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.impl = _GetchWindows()
|
||||
except ImportError:
|
||||
self.impl = _GetchUnix()
|
||||
|
||||
def __call__(self):
|
||||
return self.impl()
|
||||
|
||||
|
||||
class _GetchUnix:
|
||||
|
||||
def __init__(self):
|
||||
import tty
|
||||
del tty
|
||||
|
||||
def __call__(self):
|
||||
import tty
|
||||
import termios
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
|
||||
class _GetchWindows:
|
||||
|
||||
def __init__(self):
|
||||
import msvcrt
|
||||
del msvcrt
|
||||
|
||||
def __call__(self):
|
||||
import msvcrt
|
||||
return msvcrt.getch()
|
||||
|
||||
|
||||
getch = _Getch()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Pretty Printing
|
||||
|
||||
USE_COLOR = True
|
||||
|
||||
if USE_COLOR:
|
||||
color_codes = {
|
||||
'black': '\033[0;30m',
|
||||
'bright_gray': '\033[0;37m',
|
||||
'blue': '\033[0;34m',
|
||||
'white': '\033[1;37m',
|
||||
'green': '\033[0;32m',
|
||||
'bright_blue': '\033[1;34m',
|
||||
'cyan': '\033[0;36m',
|
||||
'bright_green': '\033[1;32m',
|
||||
'red': '\033[0;31m',
|
||||
'bright_cyan': '\033[1;36m',
|
||||
'purple': '\033[0;35m',
|
||||
'bright_red': '\033[1;31m',
|
||||
'yellow': '\033[0;33m',
|
||||
'bright_purple': '\033[1;35m',
|
||||
'dark_gray': '\033[1;30m',
|
||||
'bright_yellow': '\033[1;33m',
|
||||
'normal': '\033[0m',
|
||||
}
|
||||
|
||||
def colorize(msg, color=None):
|
||||
return (color_codes[color] + msg + color_codes['normal'])
|
||||
else:
|
||||
def colorize(msg, color=None):
|
||||
return msg
|
||||
bugfix = ""
|
||||
# avoid encoding issues
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
|
||||
sys.stdin = os.fdopen(sys.stdin.fileno(), "rb")
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
|
||||
|
||||
def print_commit(c):
|
||||
print("------------------------------------------------------------------------------")
|
||||
print(colorize("{{GitCommit|%s}}" % c.sha1.decode(), color='green'), end=" ")
|
||||
# print("Author: %s" % colorize(c.author, color='bright_blue'))
|
||||
print(colorize(c.author, color='bright_blue'))
|
||||
print()
|
||||
print(colorize(c.body, color='normal'))
|
||||
print()
|
||||
print(colorize("Files: (%d)" % len(c.files_status), color='yellow'))
|
||||
for f in c.files_status:
|
||||
print(colorize(" %s %s" % (f[0].decode('ascii'), f[1].decode('ascii')), 'yellow'))
|
||||
print()
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = "Review revisions."
|
||||
|
||||
epilog = "This script is typically used to help write release notes"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog)
|
||||
|
||||
parser.add_argument(
|
||||
"--source", dest="source_dir",
|
||||
metavar='PATH', required=True,
|
||||
help="Path to git repository")
|
||||
parser.add_argument(
|
||||
"--range", dest="range_sha1",
|
||||
metavar='SHA1_RANGE', required=True,
|
||||
help="Range to use, eg: 169c95b8..HEAD")
|
||||
parser.add_argument(
|
||||
"--author", dest="author",
|
||||
metavar='AUTHOR', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', todo]"))
|
||||
parser.add_argument(
|
||||
"--filter", dest="filter_type",
|
||||
metavar='FILTER', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', todo]"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
ACCEPT_FILE = "review_accept.txt"
|
||||
REJECT_FILE = "review_reject.txt"
|
||||
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
from git_log import GitCommitIter
|
||||
|
||||
# --------------
|
||||
# Filter Commits
|
||||
|
||||
def match(c):
|
||||
# filter_type
|
||||
if not args.filter_type:
|
||||
pass
|
||||
elif args.filter_type == 'BUGFIX':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith(("fix", "bugfix", "bug-fix"))):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
elif args.filter_type == 'NOISE':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith("cleanup")):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise Exception("Filter type %r isn't known" % args.filter_type)
|
||||
|
||||
# author
|
||||
if not args.author:
|
||||
pass
|
||||
elif args.author != c.author:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
commits = [c for c in GitCommitIter(args.source_dir, args.range_sha1) if match(c)]
|
||||
|
||||
# oldest first
|
||||
commits.reverse()
|
||||
|
||||
tot_accept = 0
|
||||
tot_reject = 0
|
||||
|
||||
def exit_message():
|
||||
print(" Written",
|
||||
colorize(ACCEPT_FILE, color='green'), "(%d)" % tot_accept,
|
||||
colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject,
|
||||
)
|
||||
|
||||
for i, c in enumerate(commits):
|
||||
if os.name == "posix":
|
||||
# Also clears scroll-back.
|
||||
os.system("tput reset")
|
||||
else:
|
||||
print('\x1b[2J') # clear
|
||||
|
||||
sha1 = c.sha1
|
||||
|
||||
# diff may scroll off the screen, that's OK
|
||||
os.system("git --git-dir %s show %s --format=%%n" % (c._git_dir, sha1.decode('ascii')))
|
||||
print("")
|
||||
print_commit(c)
|
||||
sys.stdout.flush()
|
||||
# print(ch)
|
||||
while True:
|
||||
print("Space=" + colorize("Accept", 'green'),
|
||||
"Enter=" + colorize("Skip", 'red'),
|
||||
"Ctrl+C or Q=" + colorize("Quit", color='white'),
|
||||
"[%d of %d]" % (i + 1, len(commits)),
|
||||
"(+%d | -%d)" % (tot_accept, tot_reject),
|
||||
)
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x03' or ch == b'q':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return
|
||||
|
||||
elif ch == b' ':
|
||||
log_filepath = ACCEPT_FILE
|
||||
tot_accept += 1
|
||||
break
|
||||
elif ch == b'\r':
|
||||
log_filepath = REJECT_FILE
|
||||
tot_reject += 1
|
||||
break
|
||||
else:
|
||||
print("Unknown input %r" % ch)
|
||||
|
||||
with open(log_filepath, 'ab') as f:
|
||||
f.write(sha1 + b'\n')
|
||||
|
||||
exit_message()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
772
blender-5.2.0/tools/utils/git_log_review_commits_advanced.py
Executable file
772
blender-5.2.0/tools/utils/git_log_review_commits_advanced.py
Executable file
@@ -0,0 +1,772 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
This is a tool for reviewing commit ranges, writing into accept/reject files,
|
||||
and optionally generate release-log-ready data.
|
||||
|
||||
Useful for reviewing revisions to back-port to stable builds.
|
||||
|
||||
Note that, if any of the data files generated already exist, they will be extended
|
||||
with new revisions, not overwritten.
|
||||
|
||||
Note that, for the most complex 'wiki-ready' file generated by `--accept-releaselog`,
|
||||
proof-reading after this tool has ran is heavily suggested!
|
||||
|
||||
Example usage:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79
|
||||
|
||||
To add list of fixes between RC2 and RC3, and list both RC2 and RC3 fixes also in their own sections:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range <RC2 revision>..<RC3 revision> --filter 'BUGFIX' --accept-pretty --accept-releaselog --blender-rev 2.79 --blender-rstate=RC3 --blender-rstate-list="RC2,RC3"
|
||||
|
||||
To exclude all commits from some given files, by sha1 or by commit message (from previously generated release logs) - much handy when going over commits which were partially cherry-picked into a previous release branch e.g.:
|
||||
|
||||
./git_log_review_commits_advanced.py --source ../../.. --range HEAD~40..HEAD --filter 'BUGFIX' --filter-exclude-sha1-fromfiles "review_accept.txt" "review_reject.txt" --filter-exclude-fromreleaselogs "review_accept_release_log.txt" --accept-pretty --accept-releaselog --blender-rev 2.75
|
||||
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import re
|
||||
|
||||
ACCEPT_FILE = "review_accept.txt"
|
||||
REJECT_FILE = "review_reject.txt"
|
||||
ACCEPT_LOG_FILE = "review_accept_log.txt"
|
||||
ACCEPT_PRETTY_FILE = "review_accept_pretty.txt"
|
||||
ACCEPT_RELEASELOG_FILE = "review_accept_release_log.txt"
|
||||
|
||||
IGNORE_START_LINE = "<!-- IGNORE_START -->"
|
||||
IGNORE_END_LINE = "<!-- IGNORE_END -->"
|
||||
|
||||
_cwd = os.getcwd()
|
||||
__doc__ = __doc__ + \
|
||||
"\nRaw GIT revisions files:\n\t* Accepted: %s\n\t* Rejected: %s\n\n" \
|
||||
"Basic log accepted revisions: %s\n\nWiki-printed accepted revisions: %s\n\n" \
|
||||
"Full release notes wiki page: %s\n" \
|
||||
% (os.path.join(_cwd, ACCEPT_FILE), os.path.join(_cwd, REJECT_FILE),
|
||||
os.path.join(_cwd, ACCEPT_LOG_FILE), os.path.join(_cwd, ACCEPT_PRETTY_FILE),
|
||||
os.path.join(_cwd, ACCEPT_RELEASELOG_FILE))
|
||||
del _cwd
|
||||
|
||||
|
||||
class _Getch:
|
||||
"""
|
||||
Gets a single character from standard input.
|
||||
Does not echo to the screen.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.impl = _GetchWindows()
|
||||
except ImportError:
|
||||
self.impl = _GetchUnix()
|
||||
|
||||
def __call__(self):
|
||||
return self.impl()
|
||||
|
||||
|
||||
class _GetchUnix:
|
||||
|
||||
def __init__(self):
|
||||
import tty
|
||||
del tty
|
||||
|
||||
def __call__(self):
|
||||
import tty
|
||||
import termios
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
return ch
|
||||
|
||||
|
||||
class _GetchWindows:
|
||||
|
||||
def __init__(self):
|
||||
import msvcrt
|
||||
del msvcrt
|
||||
|
||||
def __call__(self):
|
||||
import msvcrt
|
||||
return msvcrt.getch()
|
||||
|
||||
|
||||
getch = _Getch()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Pretty Printing
|
||||
|
||||
USE_COLOR = True
|
||||
|
||||
if USE_COLOR:
|
||||
color_codes = {
|
||||
'black': '\033[0;30m',
|
||||
'bright_gray': '\033[0;37m',
|
||||
'blue': '\033[0;34m',
|
||||
'white': '\033[1;37m',
|
||||
'green': '\033[0;32m',
|
||||
'bright_blue': '\033[1;34m',
|
||||
'cyan': '\033[0;36m',
|
||||
'bright_green': '\033[1;32m',
|
||||
'red': '\033[0;31m',
|
||||
'bright_cyan': '\033[1;36m',
|
||||
'purple': '\033[0;35m',
|
||||
'bright_red': '\033[1;31m',
|
||||
'yellow': '\033[0;33m',
|
||||
'bright_purple': '\033[1;35m',
|
||||
'dark_gray': '\033[1;30m',
|
||||
'bright_yellow': '\033[1;33m',
|
||||
'normal': '\033[0m',
|
||||
}
|
||||
|
||||
def colorize(msg, color=None):
|
||||
return (color_codes[color] + msg + color_codes['normal'])
|
||||
else:
|
||||
def colorize(msg, color=None):
|
||||
return msg
|
||||
bugfix = ""
|
||||
|
||||
|
||||
BUGFIX_CATEGORIES = (
|
||||
("Objects / Animation / GP", (
|
||||
"Animation",
|
||||
"Constraints",
|
||||
"Grease Pencil",
|
||||
"Objects",
|
||||
"Dependency Graph",
|
||||
),
|
||||
),
|
||||
|
||||
("Data / Geometry", (
|
||||
"Armatures",
|
||||
"Curve/Text Editing",
|
||||
"Mesh Editing",
|
||||
"Meta Editing",
|
||||
"Modifiers",
|
||||
"Material / Texture",
|
||||
),
|
||||
),
|
||||
|
||||
("Physics / Simulations / Sculpt / Paint", (
|
||||
"Particles",
|
||||
"Physics / Hair / Simulations",
|
||||
"Sculpting / Painting",
|
||||
),
|
||||
),
|
||||
|
||||
("Image / Video / Render", (
|
||||
"Image / UV Editing",
|
||||
"Masking",
|
||||
"Motion Tracking",
|
||||
"Movie Clip Editor",
|
||||
"Nodes / Compositor",
|
||||
"Render",
|
||||
"Render: Cycles",
|
||||
"Render: Freestyle",
|
||||
"Sequencer",
|
||||
),
|
||||
),
|
||||
|
||||
("UI / Spaces / Transform", (
|
||||
"3D View",
|
||||
"Input (NDOF / 3D Mouse)",
|
||||
"Outliner",
|
||||
"Text Editor",
|
||||
"Transform",
|
||||
"User Interface",
|
||||
),
|
||||
),
|
||||
|
||||
("Game Engine", (
|
||||
),
|
||||
),
|
||||
|
||||
("System / Misc", (
|
||||
"Audio",
|
||||
"File I/O",
|
||||
"Other",
|
||||
"Python",
|
||||
"System",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
sys.stdin = os.fdopen(sys.stdin.fileno(), "rb")
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='surrogateescape', line_buffering=True)
|
||||
|
||||
|
||||
def gen_commit_summary(c):
|
||||
# In git, all commit message lines until first empty one are part of 'summary'.
|
||||
return c.body.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
|
||||
|
||||
|
||||
def print_commit(c):
|
||||
print("------------------------------------------------------------------------------")
|
||||
print(colorize(c.sha1.decode(), color='green'), end=" ")
|
||||
print(colorize(c.date.strftime("%Y/%m/%d"), color='purple'), end=" ")
|
||||
print(colorize(c.author, color='bright_blue'))
|
||||
print()
|
||||
print(colorize(c.body, color='normal'))
|
||||
print()
|
||||
print(colorize("Files: (%d)" % len(c.files_status), color='yellow'))
|
||||
for f in c.files_status:
|
||||
print(colorize(" %s %s" % (f[0].decode('ascii'), f[1].decode('ascii')), 'yellow'))
|
||||
print()
|
||||
|
||||
|
||||
def gen_commit_log(c):
|
||||
return "rB%s %s %-30s %s" % (c.sha1.decode()[:10], c.date.strftime("%Y/%m/%d"),
|
||||
c.author, gen_commit_summary(c))
|
||||
|
||||
|
||||
re_bugify_str = r"T([0-9]{1,})"
|
||||
re_bugify = re.compile(re_bugify_str)
|
||||
re_commitify = re.compile(r"\W(r(?:B|BA|BAC|BTS)[0-9a-fA-F]{6,})")
|
||||
re_prettify = re.compile(r"(.{,20}?)(Fix(?:ing|es)?\s*(?:for)?\s*" + re_bugify_str + r")\s*[-:,]*\s*", re.IGNORECASE)
|
||||
|
||||
|
||||
def gen_commit_message_pretty(c, unreported=None):
|
||||
body = gen_commit_summary(c)
|
||||
|
||||
tbody = re_prettify.sub(r"Fix {{BugReport|\3}}: \1", body)
|
||||
if tbody == body:
|
||||
if unreported is not None:
|
||||
unreported[0] = True
|
||||
tbody = "Fix unreported: %s" % body
|
||||
body = re_bugify.sub(r"{{BugReport|\1}}", tbody)
|
||||
body = re_commitify.sub(r"{{GitCommit|\1}}", body)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def gen_commit_pretty(c, unreported=None, rstate=None):
|
||||
body = gen_commit_message_pretty(c, unreported)
|
||||
|
||||
if rstate is not None:
|
||||
return "* [%s] %s ({{GitCommit|rB%s}})." % (rstate, body, c.sha1.decode()[:10])
|
||||
return "* %s ({{GitCommit|rB%s}})." % (body, c.sha1.decode()[:10])
|
||||
|
||||
|
||||
def gen_commit_unprettify(body):
|
||||
if body.startswith("* ["):
|
||||
end = body.find("]")
|
||||
if end > 0:
|
||||
body = body[end + 2:] # +2 to remove ] itself, and following space.
|
||||
start = body.rfind("({{GitCommit|rB")
|
||||
if start > 0:
|
||||
body = body[:start - 1] # -1 to remove trailing space.
|
||||
return body
|
||||
|
||||
|
||||
def print_categories_tree():
|
||||
for i, (main_cat, sub_cats) in enumerate(BUGFIX_CATEGORIES):
|
||||
print("\t[%d] %s" % (i, main_cat))
|
||||
for j, sub_cat in enumerate(sub_cats):
|
||||
print("\t\t[%d] %s" % (j, sub_cat))
|
||||
|
||||
|
||||
def release_log_extract_messages(path):
|
||||
messages = set()
|
||||
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
ignore = False
|
||||
header = True
|
||||
for l in f:
|
||||
if IGNORE_END_LINE in l:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in l:
|
||||
ignore = True
|
||||
continue
|
||||
l = l.strip(" \n")
|
||||
if header and not l.startswith("=="):
|
||||
continue # Header, we don't care here.
|
||||
header = False
|
||||
if not l.startswith("==") and "Fix " in l:
|
||||
messages.add(gen_commit_unprettify(l))
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def release_log_init(path, source_dir, blender_rev, start_sha1, end_sha1, rstate, rstate_list):
|
||||
from git_log import GitRepo
|
||||
|
||||
if rstate is not None:
|
||||
header = "= Blender %s: Bug Fixes =\n\n" \
|
||||
"[%s] Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
|
||||
% (blender_rev, rstate, start_sha1[:10], end_sha1[:10])
|
||||
else:
|
||||
header = "= Blender %s: Bug Fixes =\n\n" \
|
||||
"Changes from revision {{GitCommit|rB%s}} to {{GitCommit|rB%s}}, inclusive.\n\n" \
|
||||
% (blender_rev, start_sha1[:10], end_sha1[:10])
|
||||
|
||||
release_log = {"__HEADER__": header, "__COUNT__": [0, 0], "__RSTATES__": {k: [] for k in rstate_list}}
|
||||
|
||||
if os.path.exists(path):
|
||||
branch = GitRepo(source_dir).branch.decode().strip()
|
||||
|
||||
sub_cats_to_main_cats = {s_cat: m_cat[0] for m_cat in BUGFIX_CATEGORIES for s_cat in m_cat[1]}
|
||||
main_cats = {m_cat[0] for m_cat in BUGFIX_CATEGORIES}
|
||||
with open(path, 'r') as f:
|
||||
header = []
|
||||
main_cat = None
|
||||
sub_cat = None
|
||||
ignore = False
|
||||
for l in f:
|
||||
if IGNORE_END_LINE in l:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in l:
|
||||
ignore = True
|
||||
continue
|
||||
l = l.strip(" \n")
|
||||
if not header:
|
||||
header.append(l)
|
||||
for hl in f:
|
||||
if IGNORE_END_LINE in hl:
|
||||
ignore = False
|
||||
continue
|
||||
elif ignore or IGNORE_START_LINE in hl:
|
||||
ignore = True
|
||||
continue
|
||||
hl = hl.strip(" \n")
|
||||
if hl.startswith("=="):
|
||||
main_cat = hl.strip(" =")
|
||||
if main_cat not in main_cats:
|
||||
sub_cat = main_cat
|
||||
main_cat = sub_cats_to_main_cats.get(main_cat, None)
|
||||
else:
|
||||
sub_cat = None
|
||||
# print("hl MAINCAT:", hl, main_cat, " | ", sub_cat)
|
||||
break
|
||||
header.append(hl)
|
||||
|
||||
if rstate is not None:
|
||||
release_log["__HEADER__"] = "%s[%s] Changes from revision {{GitCommit|%s}} to " \
|
||||
"{{GitCommit|%s}}, inclusive (''%s'' branch).\n\n" \
|
||||
"" % ("\n".join(header), rstate,
|
||||
start_sha1[:10], end_sha1[:10], branch)
|
||||
else:
|
||||
release_log["__HEADER__"] = "%sChanges from revision {{GitCommit|%s}} to {{GitCommit|%s}}, " \
|
||||
"inclusive (''%s'' branch).\n\n" \
|
||||
"" % ("\n".join(header), start_sha1[:10], end_sha1[:10], branch)
|
||||
count = release_log["__COUNT__"] = [0, 0]
|
||||
continue
|
||||
|
||||
if l.startswith("==="):
|
||||
sub_cat = l.strip(" =")
|
||||
if sub_cat in sub_cats_to_main_cats:
|
||||
main_cat = sub_cats_to_main_cats.get(sub_cat, None)
|
||||
elif sub_cat in main_cats:
|
||||
main_cat = sub_cat
|
||||
sub_cat = None
|
||||
else:
|
||||
main_cat = None
|
||||
# print("l SUBCAT:", l, main_cat, " | ", sub_cat)
|
||||
elif l.startswith("=="):
|
||||
main_cat = l.strip(" =")
|
||||
if main_cat not in main_cats:
|
||||
sub_cat = main_cat
|
||||
main_cat = sub_cats_to_main_cats.get(main_cat, None)
|
||||
else:
|
||||
sub_cat = None
|
||||
# print("l MAINCAT:", l, main_cat, " | ", sub_cat)
|
||||
elif "Fix " in l:
|
||||
if "Fix {{BugReport|" in l:
|
||||
main_cat_data, _ = release_log.setdefault(main_cat, ({}, {}))
|
||||
main_cat_data.setdefault(sub_cat, []).append(l)
|
||||
count[0] += 1
|
||||
# print("l REPORTED:", l)
|
||||
else:
|
||||
_, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
|
||||
main_cat_data_unreported.setdefault(sub_cat, []).append(l)
|
||||
count[1] += 1
|
||||
# print("l UNREPORTED:", l)
|
||||
l_rstate = l.strip("* ")
|
||||
if l_rstate.startswith("["):
|
||||
end = l_rstate.find("]")
|
||||
if end > 0:
|
||||
rstate = l_rstate[1:end]
|
||||
if rstate in release_log["__RSTATES__"]:
|
||||
release_log["__RSTATES__"][rstate].append("* %s" % l_rstate[end + 1:].strip())
|
||||
|
||||
return release_log
|
||||
|
||||
|
||||
def write_release_log(path, release_log, c, cat, rstate, rstate_list):
|
||||
main_cat, sub_cats = BUGFIX_CATEGORIES[cat[0]]
|
||||
sub_cat = sub_cats[cat[1]] if cat[1] is not None else None
|
||||
|
||||
main_cat_data, main_cat_data_unreported = release_log.setdefault(main_cat, ({}, {}))
|
||||
unreported = [False]
|
||||
entry = gen_commit_pretty(c, unreported, rstate)
|
||||
if unreported[0]:
|
||||
main_cat_data_unreported.setdefault(sub_cat, []).append(entry)
|
||||
release_log["__COUNT__"][1] += 1
|
||||
else:
|
||||
main_cat_data.setdefault(sub_cat, []).append(entry)
|
||||
release_log["__COUNT__"][0] += 1
|
||||
|
||||
if rstate in release_log["__RSTATES__"]:
|
||||
release_log["__RSTATES__"][rstate].append(gen_commit_pretty(c))
|
||||
|
||||
lines = []
|
||||
main_cat_lines = []
|
||||
sub_cat_lines = []
|
||||
for main_cat, sub_cats in BUGFIX_CATEGORIES:
|
||||
main_cat_data = release_log.get(main_cat, ({}, {}))
|
||||
main_cat_lines[:] = ["== %s ==" % main_cat]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(None, [])
|
||||
if entries:
|
||||
main_cat_lines.extend(entries)
|
||||
main_cat_lines.append("")
|
||||
if len(main_cat_lines) == 1:
|
||||
main_cat_lines.append("")
|
||||
for sub_cat in sub_cats:
|
||||
sub_cat_lines[:] = ["=== %s ===" % sub_cat]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(sub_cat, [])
|
||||
if entries:
|
||||
sub_cat_lines.extend(entries)
|
||||
sub_cat_lines.append("")
|
||||
if len(sub_cat_lines) > 2:
|
||||
main_cat_lines += sub_cat_lines
|
||||
if len(main_cat_lines) > 2:
|
||||
lines += main_cat_lines
|
||||
|
||||
if None in release_log:
|
||||
main_cat_data = release_log.get(None, ({}, {}))
|
||||
main_cat_lines[:] = ["== %s ==\n\n" % "UNSORTED"]
|
||||
for data in main_cat_data:
|
||||
entries = data.get(None, [])
|
||||
if entries:
|
||||
main_cat_lines.extend(entries)
|
||||
main_cat_lines.append("")
|
||||
if len(main_cat_lines) > 2:
|
||||
lines += main_cat_lines
|
||||
|
||||
with open(path, 'w') as f:
|
||||
f.write(release_log["__HEADER__"])
|
||||
|
||||
count = release_log["__COUNT__"]
|
||||
f.write("%s\n" % IGNORE_START_LINE)
|
||||
f.write("Total fixed bugs: %d (%d from tracker, %d reported/found by other ways).\n\n"
|
||||
"" % (sum(count), count[0], count[1]))
|
||||
f.write("%s\n%s\n\n" % ("{{Note|Note|Before RC1 (i.e. during regular development of next version in main "
|
||||
"branch), only fixes of issues which already existed in previous official releases are "
|
||||
"listed here. Fixes for regressions introduced since last release, or for new "
|
||||
"features, are '''not''' listed here.<br/>For following RCs and final release, "
|
||||
"'''all''' back-ported fixes are listed.}}", IGNORE_END_LINE))
|
||||
|
||||
f.write("\n".join(lines))
|
||||
f.write("\n")
|
||||
|
||||
f.write("%s\n\n<hr/>\n\n" % IGNORE_START_LINE)
|
||||
for rst in rstate_list:
|
||||
entries = release_log["__RSTATES__"].get(rst, [])
|
||||
if entries:
|
||||
f.write("== %s ==\n" % rst)
|
||||
f.write("For %s, %d bugs were fixed:\n\n" % (rst, len(entries)))
|
||||
f.write("\n".join(entries))
|
||||
f.write("\n\n")
|
||||
f.write("%s\n" % IGNORE_END_LINE)
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
global __doc__
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
usage_text = __doc__
|
||||
|
||||
epilog = "This script is typically used to help write release notes"
|
||||
|
||||
parser = argparse.ArgumentParser(description=usage_text, epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
parser.add_argument(
|
||||
"--source", dest="source_dir",
|
||||
metavar='PATH', required=True,
|
||||
help="Path to git repository")
|
||||
parser.add_argument(
|
||||
"--range", dest="range_sha1",
|
||||
metavar='SHA1_RANGE', required=False,
|
||||
help="Range to use, eg: 169c95b8..HEAD")
|
||||
parser.add_argument(
|
||||
"--author", dest="author",
|
||||
metavar='AUTHOR', type=str, required=False,
|
||||
help=("Author(s) to filter commits ("))
|
||||
parser.add_argument(
|
||||
"--filter", dest="filter_type",
|
||||
metavar='FILTER', type=str, required=False,
|
||||
help=("Method to filter commits in ['BUGFIX', 'NOISE']"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-sha1", dest="filter_exclude_sha1_list",
|
||||
default=[], required=False, type=lambda s: s.split(","),
|
||||
help=("Coma-separated list of commits to ignore/skip"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-sha1-fromfiles", dest="filter_exclude_sha1_filepaths",
|
||||
default="", required=False, nargs='*',
|
||||
help=("One or more text files storing list of commits to ignore/skip"))
|
||||
parser.add_argument(
|
||||
"--filter-exclude-fromreleaselogs", dest="filter_exclude_releaselogs",
|
||||
default="", required=False, nargs='*',
|
||||
help=("One or more text files storing release logs, to ignore/skip their entries "
|
||||
"(based on message comparison, not commit sha1)"))
|
||||
parser.add_argument(
|
||||
"--accept-log", dest="accept_log",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output more complete info about accepted commits (summary, author...)"))
|
||||
parser.add_argument(
|
||||
"--accept-pretty", dest="accept_pretty",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output pretty-printed accepted commits (nearly ready for WIKI release notes)"))
|
||||
parser.add_argument(
|
||||
"--accept-releaselog", dest="accept_releaselog",
|
||||
default=False, action='store_true', required=False,
|
||||
help=("Also output accepted commits as a wiki release log page (adds sorting by categories)"))
|
||||
parser.add_argument(
|
||||
"--blender-rev", dest="blender_rev",
|
||||
default=None, required=False,
|
||||
help=("Blender revision (only used to generate release notes page)"))
|
||||
parser.add_argument(
|
||||
"--blender-rstate", dest="blender_rstate",
|
||||
default="alpha", required=False,
|
||||
help=("Blender release state (like alpha, beta, rc1, final, corr_a, corr_b, etc.), "
|
||||
"each revision will be tagged by given one"))
|
||||
parser.add_argument(
|
||||
"--blender-rstate-list", dest="blender_rstate_list",
|
||||
default="", required=False, type=lambda s: s.split(","),
|
||||
help=("Blender release state(s) to additionally list in their own sections "
|
||||
"(e.g. pass 'RC2' to list fixes between RC1 and RC2, ie tagged as RC2, etc.)"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
# ----------
|
||||
# Parse Args
|
||||
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
for path in args.filter_exclude_sha1_filepaths:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
args.filter_exclude_sha1_list += [sha1 for l in f for sha1 in l.split()]
|
||||
args.filter_exclude_sha1_list = {sha1.encode() for sha1 in args.filter_exclude_sha1_list}
|
||||
|
||||
messages = set()
|
||||
for path in args.filter_exclude_releaselogs:
|
||||
messages |= release_log_extract_messages(path)
|
||||
args.filter_exclude_releaselogs = messages
|
||||
|
||||
from git_log import GitCommitIter
|
||||
|
||||
# --------------
|
||||
# Filter Commits
|
||||
|
||||
def match(c):
|
||||
# filter_type
|
||||
if not args.filter_type:
|
||||
pass
|
||||
elif args.filter_type == 'BUGFIX':
|
||||
first_line = c.body.split("\n\n")[0].strip(" :.;-\n").replace("\n", " ")
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith(("fix", "bugfix", "bug-fix"))):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
elif args.filter_type == 'NOISE':
|
||||
first_line = c.body.strip().split("\n")[0]
|
||||
assert len(first_line)
|
||||
if any(w for w in first_line.split() if w.lower().startswith("cleanup")):
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
raise Exception("Filter type %r isn't known" % args.filter_type)
|
||||
|
||||
# author
|
||||
if not args.author:
|
||||
pass
|
||||
elif args.author != c.author:
|
||||
return False
|
||||
|
||||
# commits to exclude
|
||||
if c.sha1 in args.filter_exclude_sha1_list:
|
||||
return False
|
||||
|
||||
# exclude by commit message (because cherry-pick totally breaks relations with original commit...)
|
||||
if args.filter_exclude_releaselogs:
|
||||
if gen_commit_message_pretty(c) in args.filter_exclude_releaselogs:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if args.accept_releaselog:
|
||||
blender_rev = args.blender_rev or "<UNKNOWN>"
|
||||
commits = tuple(GitCommitIter(args.source_dir, args.range_sha1))
|
||||
release_log = release_log_init(ACCEPT_RELEASELOG_FILE, args.source_dir, blender_rev,
|
||||
commits[-1].sha1.decode(), commits[0].sha1.decode(),
|
||||
args.blender_rstate, args.blender_rstate_list)
|
||||
commits = [c for c in commits if match(c)]
|
||||
else:
|
||||
commits = [c for c in GitCommitIter(args.source_dir, args.range_sha1) if match(c)]
|
||||
|
||||
# oldest first
|
||||
commits.reverse()
|
||||
|
||||
tot_accept = 0
|
||||
tot_reject = 0
|
||||
|
||||
def exit_message():
|
||||
print(" Written",
|
||||
colorize(ACCEPT_FILE, color='green'), "(%d)" % tot_accept,
|
||||
colorize(ACCEPT_LOG_FILE, color='yellow'), "(%d)" % tot_accept,
|
||||
colorize(ACCEPT_PRETTY_FILE, color='blue'), "(%d)" % tot_accept,
|
||||
colorize(REJECT_FILE, color='red'), "(%d)" % tot_reject,
|
||||
)
|
||||
|
||||
def get_cat(ch, max_idx):
|
||||
cat = -1
|
||||
try:
|
||||
cat = int(ch)
|
||||
except:
|
||||
pass
|
||||
if 0 <= cat < max_idx:
|
||||
return cat
|
||||
print("Invalid input %r" % ch)
|
||||
return None
|
||||
|
||||
for i, c in enumerate(commits):
|
||||
if os.name == "posix":
|
||||
# Also clears scroll-back.
|
||||
os.system("tput reset")
|
||||
else:
|
||||
print('\x1b[2J') # clear
|
||||
|
||||
sha1 = c.sha1
|
||||
|
||||
# diff may scroll off the screen, that's OK
|
||||
os.system("git --git-dir %s show %s --format=%%n" % (c._git_dir, sha1.decode('ascii')))
|
||||
print("")
|
||||
print_commit(c)
|
||||
sys.stdout.flush()
|
||||
|
||||
while True:
|
||||
print("Space=" + colorize("Accept", 'green'),
|
||||
"Enter=" + colorize("Skip", 'red'),
|
||||
"Ctrl+C or X=" + colorize("Exit", color='white'),
|
||||
"[%d of %d]" % (i + 1, len(commits)),
|
||||
"(+%d | -%d)" % (tot_accept, tot_reject),
|
||||
)
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x03' or ch == b'x':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return False
|
||||
elif ch == b' ':
|
||||
log_filepath = ACCEPT_FILE
|
||||
log_filepath_log = ACCEPT_LOG_FILE
|
||||
log_filepath_pretty = ACCEPT_PRETTY_FILE
|
||||
tot_accept += 1
|
||||
|
||||
if args.accept_releaselog: # Enter sub-loop for category selection.
|
||||
done_main = True
|
||||
c1 = c2 = None
|
||||
while True:
|
||||
if c1 is None:
|
||||
print("Select main category (V=View all categories, M=Commit message): \n\t%s"
|
||||
"" % " | ".join("[%d] %s" % (i, cat[0]) for i, cat in enumerate(BUGFIX_CATEGORIES)))
|
||||
else:
|
||||
main_cat = BUGFIX_CATEGORIES[c1][0]
|
||||
sub_cats = BUGFIX_CATEGORIES[c1][1]
|
||||
if not sub_cats:
|
||||
break
|
||||
print("[%d] %s: Select sub category "
|
||||
"(V=View all categories, M=Commit message, Enter=No sub-categories, "
|
||||
"Backspace=Select other main category): \n\t%s"
|
||||
"" % (c1, main_cat,
|
||||
" | ".join("[%d] %s" % (i, cat) for i, cat in enumerate(sub_cats))))
|
||||
|
||||
ch = getch()
|
||||
|
||||
if ch == b'\x7f': # backspace
|
||||
done_main = False
|
||||
break
|
||||
elif ch == b'\x03' or ch == b'x':
|
||||
# Ctrl+C
|
||||
exit_message()
|
||||
print("Goodbye! (%s)" % c.sha1.decode())
|
||||
return
|
||||
elif ch == b'v':
|
||||
print_categories_tree()
|
||||
print("")
|
||||
elif ch == b'm':
|
||||
print_commit(c)
|
||||
print("")
|
||||
elif c1 is None:
|
||||
c1 = get_cat(ch, len(BUGFIX_CATEGORIES))
|
||||
elif c2 is None:
|
||||
if ch == b'\r':
|
||||
break
|
||||
elif ch == b'\x7f': # backspace
|
||||
c1 = None
|
||||
continue
|
||||
c2 = get_cat(ch, len(BUGFIX_CATEGORIES[c1][1]))
|
||||
if c2 is not None:
|
||||
break
|
||||
else:
|
||||
print("BUG! this should not happen!")
|
||||
|
||||
if done_main is False:
|
||||
# Go back to main loop, this commit is no more accepted nor rejected.
|
||||
tot_accept -= 1
|
||||
continue
|
||||
|
||||
write_release_log(ACCEPT_RELEASELOG_FILE, release_log, c, (c1, c2),
|
||||
args.blender_rstate, args.blender_rstate_list)
|
||||
break
|
||||
elif ch == b'\r':
|
||||
log_filepath = REJECT_FILE
|
||||
log_filepath_log = None
|
||||
log_filepath_pretty = None
|
||||
tot_reject += 1
|
||||
break
|
||||
else:
|
||||
print("Invalid input %r" % ch)
|
||||
|
||||
with open(log_filepath, 'ab') as f:
|
||||
f.write(sha1 + b'\n')
|
||||
|
||||
if args.accept_pretty and log_filepath_pretty:
|
||||
with open(log_filepath_pretty, 'a') as f:
|
||||
f.write(gen_commit_pretty(c, rstate=args.blender_rstate) + "\n")
|
||||
|
||||
if args.accept_log and log_filepath_log:
|
||||
with open(log_filepath_log, 'a') as f:
|
||||
f.write(gen_commit_log(c) + "\n")
|
||||
|
||||
exit_message()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
254
blender-5.2.0/tools/utils/gitea_inactive_developers.py
Executable file
254
blender-5.2.0/tools/utils/gitea_inactive_developers.py
Executable file
@@ -0,0 +1,254 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Script to get all the inactive GITEA developers
|
||||
# Usage: GITEA_API_TOKEN=<yourtoken> python3 gitea_inactive_developers.py
|
||||
#
|
||||
# The API Token have the "read:org" or "admin:org" scope.
|
||||
#
|
||||
# Potential errors:
|
||||
# * 403 Client Error: That means the token doesn't have the right scope.
|
||||
# * 500 Server Error: The token is invalid.
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import sys
|
||||
import yarl
|
||||
from retry import retry as retry_decorator
|
||||
import datetime
|
||||
import dataclasses as dc
|
||||
import iso8601
|
||||
|
||||
from typing import (
|
||||
cast,
|
||||
NewType,
|
||||
TypeVar,
|
||||
)
|
||||
from collections.abc import (
|
||||
Callable,
|
||||
Iterable,
|
||||
)
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
logger = logging.getLogger(__file__)
|
||||
|
||||
|
||||
GITEA_DOMAIN = "https://projects.blender.org/"
|
||||
|
||||
|
||||
@dc.dataclass
|
||||
class TeamMember():
|
||||
id: int
|
||||
login: str
|
||||
full_name: str
|
||||
last_login: datetime.datetime
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{id};{login};{full_name};{last_login};{url}\n".format(
|
||||
id=self.id,
|
||||
login=self.login,
|
||||
full_name=self.full_name,
|
||||
last_login=self.last_login,
|
||||
url=GITEA_DOMAIN + self.login,
|
||||
)
|
||||
|
||||
|
||||
Page = NewType('Page', int)
|
||||
|
||||
F = TypeVar('F', bound=Callable[..., object])
|
||||
T = TypeVar('T', bound=object)
|
||||
|
||||
retry: Callable[[F], F] = retry_decorator(
|
||||
tries=10, delay=1, backoff=2, logger=logger)
|
||||
|
||||
|
||||
def assert_cast(typ: type[T], obj: object) -> T:
|
||||
assert isinstance(obj, typ), f'object is not of type {typ}: {obj}'
|
||||
# NOTE: MYPY warns the cast is redundant, we might consider removing it.
|
||||
return cast(T, obj) # type: ignore
|
||||
|
||||
|
||||
def get_date_object(date_string: str) -> datetime.datetime:
|
||||
result = iso8601.parse_date(date_string)
|
||||
assert isinstance(result, datetime.datetime)
|
||||
return result
|
||||
|
||||
|
||||
results_per_page = 25
|
||||
|
||||
|
||||
def get_next_page(headers: CaseInsensitiveDict[str], page: Page) -> Page | None:
|
||||
"""
|
||||
Parse the header looking for reference to next.
|
||||
"""
|
||||
total_count = int(assert_cast(str, headers.get('X-Total-Count')))
|
||||
|
||||
# If current page already accounts to all the results we need
|
||||
# there is no need for extra pages.
|
||||
if page * results_per_page >= total_count:
|
||||
return None
|
||||
|
||||
next_page = Page(page + 1)
|
||||
return next_page
|
||||
|
||||
|
||||
@retry
|
||||
def fetch_single(
|
||||
api_url: yarl.URL,
|
||||
api_token: str,
|
||||
method: str,
|
||||
data: dict[str, str],
|
||||
page: Page,
|
||||
) -> tuple[list[object], Page | None]:
|
||||
"""Generic function to query a single item from the API.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the item data.
|
||||
"""
|
||||
headers = {
|
||||
'accept': 'application/json',
|
||||
'Authorization': 'token ' + api_token,
|
||||
}
|
||||
|
||||
params: dict[str, str | int] = {
|
||||
'limit': results_per_page,
|
||||
'page': page,
|
||||
**data,
|
||||
}
|
||||
|
||||
logger.info(f"Calling {method} ({params=}).")
|
||||
response = requests.get(str(api_url / method), params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
|
||||
next_page = get_next_page(response.headers, page)
|
||||
return response_json, None if next_page is None else next_page
|
||||
|
||||
|
||||
def fetch_all(
|
||||
api_url: yarl.URL,
|
||||
api_token: str,
|
||||
method: str,
|
||||
data: dict[str, str],
|
||||
) -> Iterable[object]:
|
||||
"""Generic function to query lists from API.
|
||||
|
||||
Yields:
|
||||
response_data - the result of fetch_single()
|
||||
"""
|
||||
page: Page | None = Page(1)
|
||||
while page is not None:
|
||||
response_data, page = fetch_single(api_url, api_token, method, data, page)
|
||||
yield from response_data if response_data is not None else ()
|
||||
|
||||
|
||||
def fetch_team_members(
|
||||
api_url: yarl.URL,
|
||||
api_token: str,
|
||||
organization_name: str,
|
||||
team_name: str,
|
||||
) -> list[TeamMember]:
|
||||
"""Query API for all the members of a team.
|
||||
|
||||
Yields:
|
||||
TeamMember objects.
|
||||
"""
|
||||
|
||||
method = "orgs/{org}/teams".format(org=organization_name)
|
||||
team_id = None
|
||||
|
||||
for team in cast(
|
||||
Iterable[dict[object, object]],
|
||||
fetch_all(
|
||||
api_url,
|
||||
api_token,
|
||||
method,
|
||||
data={},
|
||||
),
|
||||
):
|
||||
if team.get('name') != team_name:
|
||||
continue
|
||||
|
||||
team_id = team.get('id')
|
||||
break
|
||||
|
||||
if team_id is None:
|
||||
logger.error('No team found with name: ' + team_name)
|
||||
sys.exit(2)
|
||||
|
||||
method = "teams/{id}/members".format(id=team_id)
|
||||
users = list()
|
||||
|
||||
for member in cast(
|
||||
Iterable[dict[object, object]],
|
||||
fetch_all(
|
||||
api_url,
|
||||
api_token,
|
||||
method,
|
||||
data={},
|
||||
),
|
||||
):
|
||||
users.append(
|
||||
TeamMember(
|
||||
id=assert_cast(int, member.get('id')),
|
||||
login=assert_cast(str, member.get('login')),
|
||||
full_name=assert_cast(str, member.get('full_name')),
|
||||
last_login=get_date_object(
|
||||
assert_cast(str, member.get('last_login'))),
|
||||
))
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def is_inactive(member: TeamMember) -> bool:
|
||||
"""
|
||||
Returns whether the member is no longer active.
|
||||
|
||||
Users are active when they logged in the past 2 years.
|
||||
"""
|
||||
tzinfo = member.last_login.tzinfo
|
||||
two_years_ago = datetime.datetime.now(tzinfo) - datetime.timedelta(days=2 * 365)
|
||||
return member.last_login < two_years_ago
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
teams = (
|
||||
"Developers",
|
||||
"Add-ons",
|
||||
"Translation",
|
||||
"Documentation",
|
||||
"Technical-Artists",
|
||||
"Contributors",
|
||||
)
|
||||
|
||||
api_token = os.environ['GITEA_API_TOKEN']
|
||||
api_url = yarl.URL(GITEA_DOMAIN + 'api/v1/')
|
||||
organization_name = "blender"
|
||||
|
||||
for team_name in teams:
|
||||
logger.warning(team_name)
|
||||
members = fetch_team_members(
|
||||
api_url, api_token, organization_name, team_name)
|
||||
|
||||
inactive_members = [str(m) for m in members if is_inactive(m)]
|
||||
|
||||
logger.warning(" Total members:" + str(len(members)))
|
||||
logger.warning(" Inactive members: " + str(len(inactive_members)))
|
||||
|
||||
file_name = team_name.lower() + ".csv"
|
||||
with open(file_name, 'w', newline='') as csv_file:
|
||||
csv_file.write("id;login;full_name;last_login;url\n")
|
||||
csv_file.writelines(inactive_members)
|
||||
logger.warning(" Output file: " + file_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
307
blender-5.2.0/tools/utils/make_cursor_gui.py
Executable file
307
blender-5.2.0/tools/utils/make_cursor_gui.py
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Created by Robert Wenzlaff (Det. Thorn).
|
||||
# Oct. 30, 2003
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import sys
|
||||
from tkinter import (
|
||||
Button,
|
||||
Canvas,
|
||||
Checkbutton,
|
||||
END,
|
||||
Frame,
|
||||
IntVar,
|
||||
Label,
|
||||
RIDGE,
|
||||
Text,
|
||||
Tk,
|
||||
)
|
||||
|
||||
color = ("black", "white", "darkgreen", "gray")
|
||||
|
||||
|
||||
class App:
|
||||
|
||||
def __init__(self, master):
|
||||
frame = Frame(master, borderwidth=5)
|
||||
frame.grid(column=0, row=0, pady=5)
|
||||
|
||||
self.state = []
|
||||
self.states = 256
|
||||
self.laststate = 2 # 0=Black, 1=White, 2=Transparent.
|
||||
|
||||
self.size = 16
|
||||
self.gridsz = 20
|
||||
|
||||
for x in range(1024):
|
||||
self.state.append(2)
|
||||
|
||||
self.screen = Canvas(frame, height=320, width=320, bg=color[2])
|
||||
self.screen.bind("<Button-1>", self.scrnclick1)
|
||||
self.screen.bind("<Button-3>", self.scrnclick2)
|
||||
self.screen.bind("<B1-Motion>", self.scrndrag)
|
||||
|
||||
for x in range(16):
|
||||
self.screen.create_line((x * 20, 0, x * 20, 320), fill=color[3])
|
||||
self.screen.create_line((0, x * 20, 320, x * 20), fill=color[3])
|
||||
|
||||
self.screen.grid(row=0, column=0, columnspan=5)
|
||||
|
||||
frame2 = Frame(master, borderwidth=5)
|
||||
frame2.grid(column=0, row=1, pady=5)
|
||||
|
||||
self.clear = Button(frame2, text="Clear", command=self.clearit)
|
||||
self.clear.grid(row=0, column=0, pady=20)
|
||||
|
||||
self.doit = Button(frame2, text="Print", command=self.doit)
|
||||
self.doit.grid(row=0, column=1, pady=20)
|
||||
|
||||
# self.doitlab = Label(frame2, text="(Output to stdout)");
|
||||
# self.doitlab.grid(row=1, column=1);
|
||||
|
||||
self.parse = Button(frame2, text="Parse", command=self.parsetext)
|
||||
self.parse.grid(row=0, column=2, pady=20)
|
||||
|
||||
self.large = 0
|
||||
self.dummy = IntVar()
|
||||
self.largeb = Checkbutton(frame2, text="Large", var=self.dummy, command=self.changesize)
|
||||
self.largeb.grid(row=0, column=3, pady=20)
|
||||
|
||||
self.prev = Canvas(frame2, height=17, width=17, bg=color[2], relief=RIDGE)
|
||||
self.prev.grid(row=0, column=4, pady=20, padx=20)
|
||||
|
||||
# DataParsers
|
||||
self.bmlabel = Label(frame2, text="Bitmap Data (paste hex from code)")
|
||||
self.bmlabel.grid(row=2, column=0, columnspan=5, sticky="W")
|
||||
|
||||
self.bmentry = Text(frame2, width=80, height=9, font="Times 8")
|
||||
self.bmentry.bind("<Leave>", self.bmtextpaste)
|
||||
self.bmentry.grid(row=3, column=0, columnspan=5, pady=5)
|
||||
|
||||
self.msklabel = Label(frame2, text="Mask Data (paste hex from code)")
|
||||
self.msklabel.grid(row=4, column=0, columnspan=5, sticky="W")
|
||||
|
||||
self.mskentry = Text(frame2, width=80, height=9, font="Times 8")
|
||||
self.mskentry.bind("<Leave>", self.msktextpaste)
|
||||
self.mskentry.grid(row=5, column=0, columnspan=5, pady=5)
|
||||
|
||||
def changesize(self):
|
||||
self.large = ~self.large
|
||||
if self.large:
|
||||
self.size = 32
|
||||
self.gridsz = 10
|
||||
self.states = 1024
|
||||
oldstate = self.state
|
||||
self.state = []
|
||||
for n in range(1024):
|
||||
col = (n // 2) % 16
|
||||
row = int(n // 64)
|
||||
self.state.append(oldstate[16 * row + col])
|
||||
oldstate = []
|
||||
else:
|
||||
self.size = 16
|
||||
self.gridsz = 20
|
||||
self.states = 256
|
||||
oldstate = self.state
|
||||
self.state = []
|
||||
for n in range(1024):
|
||||
if not ((n % 2) or ((n // 32) % 2)):
|
||||
self.state.append(oldstate[n])
|
||||
for n in range(256, 1024):
|
||||
self.state.append(2)
|
||||
oldstate = []
|
||||
|
||||
# Insert scaling here
|
||||
|
||||
self.updatescrn()
|
||||
self.prev.config(width=self.size + 1, height=self.size + 1)
|
||||
for n in range(self.states):
|
||||
self.updateprev(n)
|
||||
# self.prev.grid(row=0, column=4, padx=self.gridsz, pady=self.gridsz)
|
||||
|
||||
def scrnclick1(self, event):
|
||||
self.scrnclick(event, 1)
|
||||
|
||||
def scrnclick2(self, event):
|
||||
self.scrnclick(event, -1)
|
||||
|
||||
def scrnclick(self, event, direction):
|
||||
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
|
||||
return
|
||||
|
||||
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
|
||||
|
||||
self.state[n] += direction
|
||||
self.state[n] %= 3
|
||||
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.state[n]], outline="")
|
||||
|
||||
self.laststate = self.state[n]
|
||||
self.updateprev(n)
|
||||
|
||||
def scrndrag(self, event):
|
||||
if (event.x > 319) or (event.y > 319) or (event.x < 0) or (event.y < 0):
|
||||
return
|
||||
|
||||
n = (event.x // self.gridsz) + self.size * (event.y // self.gridsz)
|
||||
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.laststate], outline="")
|
||||
self.state[n] = self.laststate
|
||||
|
||||
self.updateprev(n)
|
||||
|
||||
def updateprev(self, n):
|
||||
x = n % self.size + 1
|
||||
y = n // self.size + 1
|
||||
|
||||
if self.large:
|
||||
pad = 12
|
||||
else:
|
||||
pad = 20
|
||||
|
||||
self.prev.create_line(x + 1, y + 1, x + 2, y + 1, fill=color[self.state[n]])
|
||||
self.prev.grid(row=0, column=4, padx=pad, pady=pad)
|
||||
|
||||
def updatescrn(self):
|
||||
self.screen.create_rectangle(0, 0, 320, 320, fill=color[2])
|
||||
for x in range(self.size):
|
||||
self.screen.create_line((x * self.gridsz, 0, x * self.gridsz, 320), fill=color[3])
|
||||
self.screen.create_line((0, x * self.gridsz, 320, x * self.gridsz), fill=color[3])
|
||||
for n in range(self.states):
|
||||
row = n % self.size
|
||||
col = n // self.size
|
||||
self.screen.create_rectangle((self.gridsz * row + 1,
|
||||
self.gridsz * col + 1,
|
||||
self.gridsz * row + self.gridsz - 1,
|
||||
self.gridsz * col + self.gridsz - 1),
|
||||
fill=color[self.state[n]], outline="")
|
||||
|
||||
def bmtextpaste(self, event):
|
||||
string = self.bmentry.get(1.0, END)
|
||||
self.bmentry.delete(1.0, END)
|
||||
string = string.replace("\t", "")
|
||||
self.bmentry.insert(END, string)
|
||||
|
||||
def msktextpaste(self, event):
|
||||
string = self.mskentry.get(1.0, END)
|
||||
self.mskentry.delete(1.0, END)
|
||||
string = string.replace("\t", "")
|
||||
self.mskentry.insert(END, string)
|
||||
|
||||
def parsetext(self):
|
||||
bmstring = self.bmentry.get(1.0, END)
|
||||
bmstring = bmstring.replace(",", " ")
|
||||
bmstring = bmstring.split()
|
||||
|
||||
mskstring = self.mskentry.get(1.0, END)
|
||||
mskstring = mskstring.replace(",", " ")
|
||||
mskstring = mskstring.split()
|
||||
|
||||
if len(bmstring) != len(mskstring):
|
||||
print("Mismatched data. Bitmap and mask must be same size,")
|
||||
return
|
||||
elif not (len(bmstring) == 32 or len(bmstring) == 128):
|
||||
print("Size Error, input must be 32 or 128 hex bytes. ")
|
||||
return
|
||||
|
||||
for n in range(self.states):
|
||||
self.state[n] = 0
|
||||
|
||||
m = 0
|
||||
for entry in bmstring:
|
||||
e = int(entry, 16)
|
||||
for bit in range(8):
|
||||
self.state[m] = (e & 1)
|
||||
e = e >> 1
|
||||
m += 1
|
||||
|
||||
m = 0
|
||||
for entry in mskstring:
|
||||
e = int(entry, 16)
|
||||
for bit in range(8):
|
||||
if not (e & 1):
|
||||
self.state[m] = 2
|
||||
e = e >> 1
|
||||
m += 1
|
||||
|
||||
self.updatescrn()
|
||||
for n in range(self.states):
|
||||
self.updateprev(n)
|
||||
|
||||
def clearit(self):
|
||||
for n in range(self.states):
|
||||
self.state[n] = 2
|
||||
self.updateprev(n)
|
||||
self.updatescrn()
|
||||
self.bmentry.delete(0.0, END)
|
||||
self.mskentry.delete(0.0, END)
|
||||
|
||||
def doit(self):
|
||||
mask = []
|
||||
bitmap = []
|
||||
numbytes = self.size * self.size // 8
|
||||
for i in range(numbytes):
|
||||
m = 0
|
||||
b = 0
|
||||
for j in range(8):
|
||||
m <<= 1
|
||||
b <<= 1
|
||||
if (self.state[(i * 8) + (7 - j)] != 2):
|
||||
m |= 1
|
||||
if (self.state[(i * 8) + (7 - j)] == 1):
|
||||
b |= 1
|
||||
# print((i * 8) + (7 - j), self.state[(i * 8) + (7 - j)], m)
|
||||
mask.append(m)
|
||||
bitmap.append(b)
|
||||
|
||||
print("\n\nstatic char bitmap[] = {", end=' ')
|
||||
for i in range(numbytes):
|
||||
b1 = bitmap[i]
|
||||
if not (i % 8):
|
||||
print("\n\t", end=' ')
|
||||
print("0x%(b1)02x, " % vars(), end=' ')
|
||||
print("\n};")
|
||||
|
||||
print("\nstatic char mask[] = {", end=' ')
|
||||
for i in range(numbytes):
|
||||
b1 = mask[i]
|
||||
if not (i % 8):
|
||||
print("\n\t", end=' ')
|
||||
print("0x%(b1)02x, " % vars(), end=' ')
|
||||
print("\n};")
|
||||
|
||||
|
||||
################## Main App #######################
|
||||
def main() -> int:
|
||||
root = Tk()
|
||||
|
||||
_app = App(root)
|
||||
root.title("Cursor Maker")
|
||||
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
276
blender-5.2.0/tools/utils/make_generate_datamodels.py
Normal file
276
blender-5.2.0/tools/utils/make_generate_datamodels.py
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""Self-bootstrapping script to run the OpenAPI-to-dataclasses code generator.
|
||||
|
||||
Run this via `<your buildtool> generate_datamodels` in your build directory.
|
||||
|
||||
This script creates its own virtualenv, installs its dependencies, and then runs
|
||||
the code generator. It processes OpenAPI spec files in YAML format (see
|
||||
`YAML_PATHS` below) to generate Python source files. Each `xxx.yaml` file will
|
||||
produce an `xxx.py` file in the same directory. These Python files also include
|
||||
the OpenAPI spec, as a Python dict.
|
||||
|
||||
The generated Python files are tracked by Git. This generator is NOT part of the
|
||||
regular Blender build process, and only needs to be run when any of the YAML
|
||||
files change.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Paths of the OpenAPI YAML files to convert to Python code. These are relative
|
||||
# to Blender's top level source directory.
|
||||
#
|
||||
# The generated Python files will be written to the same path, just with the
|
||||
# `.py` suffix.
|
||||
#
|
||||
# When adding a file here, make sure it is named `..._openapi.yaml`. That way
|
||||
# the corresponding `.py`` file is automatically marked as 'generated' in
|
||||
# `.gitattributes`.
|
||||
YAML_PATHS = [
|
||||
"scripts/modules/_bpy_internal/assets/remote_library/blender_asset_library_openapi.yaml",
|
||||
]
|
||||
|
||||
# Packages to install in the virtualenv. These are only necessary to run this
|
||||
# generator. The generated code does not depend on these.
|
||||
REQUIREMENTS = [
|
||||
"datamodel-code-generator ~= 0.53.0",
|
||||
"PyYAML ~= 6.0.2",
|
||||
"docformatter ~= 1.7.8",
|
||||
]
|
||||
|
||||
# These arguments are quite likely to be used for all code generated with this
|
||||
# generator, also later when we use this approach in other areas.
|
||||
COMMON_ARGS = [
|
||||
# Because of the Blender code standard:
|
||||
"--use-double-quotes",
|
||||
|
||||
# Make it strict unless there's a good reason not to:
|
||||
"--strict-nullable",
|
||||
|
||||
# Ignore unknown fields in the parsed JSON. This way, the code generated now
|
||||
# has a chance of being be compatible with future versions of the schema (at
|
||||
# least, when that future version just adds new stuff).
|
||||
"--allow-extra-fields",
|
||||
|
||||
# Automatically target the currently-running version of Python:
|
||||
f"--target-python-version={sys.version_info.major}.{sys.version_info.minor}",
|
||||
|
||||
# Use `list[T]` instead of `typing.List[T]`:
|
||||
"--use-standard-collections",
|
||||
|
||||
# Because we use dataclasses:
|
||||
"--output-model-type", "dataclasses.dataclass",
|
||||
|
||||
# Work around https://github.com/koxudaxi/datamodel-code-generator/issues/1870#issuecomment-2775689249
|
||||
"--use-annotated",
|
||||
|
||||
# Remove the "generated on" timestamp from the output, so that running the
|
||||
# generator is idempotent.
|
||||
"--disable-timestamp",
|
||||
|
||||
"--use-inline-field-description",
|
||||
"--use-schema-description",
|
||||
]
|
||||
|
||||
|
||||
CUSTOM_FILE_HEADER = """
|
||||
# SPDX-FileCopyrightText: {year!s} Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#
|
||||
# Generated by datamodel-codegen:
|
||||
# source filename: {source_path.name!s}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the datamodel code generator."""
|
||||
|
||||
argparser = argparse.ArgumentParser(description="Run the datamodel code generator.")
|
||||
argparser.add_argument('source_root', type=Path, help="The root of Blender's source directory")
|
||||
args = argparser.parse_args(sys.argv[1:])
|
||||
root_path: Path = args.source_root.resolve()
|
||||
|
||||
if not root_path.is_dir():
|
||||
raise SystemExit("Path {!s} should be a directory".format(root_path))
|
||||
|
||||
print("Generating data model files:")
|
||||
|
||||
py_paths: list[Path] = []
|
||||
for yaml_relpath in YAML_PATHS:
|
||||
yaml_path = root_path / yaml_relpath
|
||||
py_path = yaml_path.with_suffix(".py")
|
||||
py_paths.append(py_path)
|
||||
|
||||
print(f" {yaml_path.relative_to(root_path)} -> {py_path.name}")
|
||||
|
||||
_generate_datamodel(
|
||||
in_path=yaml_path,
|
||||
in_type="openapi",
|
||||
out_path=py_path,
|
||||
)
|
||||
|
||||
# Make sure that output from subprocesses is flushed, before outputting more
|
||||
# below. This prevents stderr and stdout going out of sync, ensuring things
|
||||
# are shown in chronological order (i.e. generating files before
|
||||
# reformatting them).
|
||||
sys.stderr.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
# Format the generated Python code.
|
||||
_docformatter(py_paths)
|
||||
_make_format(root_path, py_paths)
|
||||
|
||||
print("Done generating data model files!")
|
||||
|
||||
|
||||
def _generate_datamodel(in_path: Path, in_type: str, out_path: Path) -> None:
|
||||
"""Run datamodel-codegen."""
|
||||
|
||||
# `type: ignore` to ignore warnings that this module cannot be imported. Python checkers
|
||||
# won't understand this is run from a self-managed virtualenv.
|
||||
from datamodel_code_generator.__main__ import main as codegen_main # type: ignore
|
||||
from datamodel_code_generator.__main__ import Exit # type: ignore
|
||||
|
||||
header = CUSTOM_FILE_HEADER.strip().format(
|
||||
year=time.localtime().tm_year,
|
||||
source_path=in_path,
|
||||
)
|
||||
|
||||
args = [
|
||||
*COMMON_ARGS,
|
||||
"--input", str(in_path),
|
||||
"--input-file-type", in_type,
|
||||
"--output", str(out_path),
|
||||
"--custom-file-header", header,
|
||||
]
|
||||
|
||||
status = codegen_main(args)
|
||||
|
||||
match status:
|
||||
case Exit.OK:
|
||||
return
|
||||
case Exit.ERROR:
|
||||
raise SystemExit("code generation failed")
|
||||
case Exit.KeyboardInterrupt:
|
||||
raise KeyboardInterrupt()
|
||||
case _:
|
||||
raise SystemExit(f"unknown result from code generation: {status}")
|
||||
|
||||
|
||||
def _docformatter(py_paths: list[Path]) -> None:
|
||||
"""Run 'docformatter' on generated Python files.
|
||||
|
||||
This is necessary because the generated docstrings are very long, and
|
||||
'make format' doesn't automatically re-wrap them.
|
||||
"""
|
||||
from docformatter import format
|
||||
from docformatter import configuration
|
||||
|
||||
print("Formatting docstrings")
|
||||
|
||||
argv = ["docformatter", "--in-place", *(str(path) for path in py_paths)]
|
||||
cfg = configuration.Configurater(argv)
|
||||
cfg.do_parse_arguments()
|
||||
|
||||
formatter = format.Formatter(
|
||||
cfg.args,
|
||||
stderror=sys.stderr,
|
||||
stdin=sys.stdin,
|
||||
stdout=sys.stdout,
|
||||
)
|
||||
result = formatter.do_format_files()
|
||||
if result not in {format.FormatResult.ok, format.FormatResult.format_required}:
|
||||
raise RuntimeError(f"Error {result} running docformatter")
|
||||
|
||||
|
||||
def _make_format(root_path: Path, py_paths: list[Path]) -> None:
|
||||
"""Run 'make format' on generated Python files."""
|
||||
|
||||
print("Formatting Python files")
|
||||
|
||||
py_paths_as_str = [str(path) for path in py_paths]
|
||||
subprocess.run(
|
||||
["make", "format", "PATHS={}".format(" ".join(py_paths_as_str))],
|
||||
cwd=root_path,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
# --------- Below this point is the self-bootstrapping logic ---------
|
||||
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import venv
|
||||
|
||||
# Name of a module to import, to test whether dependencies have been installed or not.
|
||||
TEST_INSTALL_MODULE = "datamodel_code_generator"
|
||||
|
||||
# Directory for the virtualenv. This script is expected to run with Blender's
|
||||
# build directory as its working directory.
|
||||
VENV_DIR = Path("generate_datamodels_venv").resolve()
|
||||
|
||||
# Python executable inside the virtual environment.
|
||||
VENV_PYTHON = VENV_DIR / "Scripts/python.exe" if sys.platform == "win32" else VENV_DIR / "bin/python"
|
||||
|
||||
|
||||
def _create_virtualenv() -> None:
|
||||
"""Create the virtual environment if it does not exist."""
|
||||
if VENV_DIR.exists():
|
||||
return
|
||||
print(f"Creating virtual environment at {VENV_DIR}")
|
||||
venv.create(VENV_DIR, with_pip=True)
|
||||
|
||||
|
||||
def _install_dependencies() -> None:
|
||||
"""Install required dependencies into the virtual environment."""
|
||||
print("Installing dependencies")
|
||||
# Pip doesn't like to be used as Python library, invoking it via the CLI is the best option.
|
||||
_run_command(str(VENV_PYTHON), "-m", "pip", "install", "--upgrade", "pip")
|
||||
_run_command(str(VENV_PYTHON), "-m", "pip", "install", "--upgrade", *REQUIREMENTS)
|
||||
|
||||
|
||||
def _is_dependency_installed(package: str) -> bool:
|
||||
"""Try importing a package to check if it is installed."""
|
||||
return importlib.util.find_spec(package) is not None
|
||||
|
||||
|
||||
def _is_running_in_virtualenv() -> bool:
|
||||
"""Check if the script is running inside the virtual environment."""
|
||||
return sys.prefix != sys.base_prefix # Virtualenv modifies `sys.prefix`
|
||||
|
||||
|
||||
def _run_command(*cmd: str) -> None:
|
||||
"""Run a shell command and handle errors."""
|
||||
try:
|
||||
subprocess.run(cmd, check=True, text=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running command: {' '.join(cmd)}", file=sys.stderr)
|
||||
print(f"Exit code: {e.returncode}", file=sys.stderr)
|
||||
sys.exit(e.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_create_virtualenv()
|
||||
|
||||
if not _is_running_in_virtualenv():
|
||||
print(f"Re-executing inside virtual environment at {VENV_DIR}")
|
||||
_run_command(str(VENV_PYTHON), *sys.argv)
|
||||
sys.exit()
|
||||
|
||||
if not _is_dependency_installed(TEST_INSTALL_MODULE):
|
||||
_install_dependencies()
|
||||
|
||||
# The virtual environment is active, so run the main script logic.
|
||||
main()
|
||||
82
blender-5.2.0/tools/utils/make_gl_stipple_from_xpm.py
Normal file
82
blender-5.2.0/tools/utils/make_gl_stipple_from_xpm.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Converts 32x32 XPM images written be the gimp to GL stipples
|
||||
# takes XPM files as arguments, prints out C style definitions.
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
xpm_ls = [f for f in sys.argv[1:] if f.lower().endswith(".xpm")]
|
||||
|
||||
print("Converting:", " ".join(xpm_ls))
|
||||
|
||||
for xpm in xpm_ls:
|
||||
with open(xpm, "r", encoding="utf-8") as fh:
|
||||
data = fh.read()
|
||||
|
||||
# all after first {
|
||||
data = data.split("{", 1)[1]
|
||||
|
||||
# all before first }
|
||||
data = data.rsplit("}", 1)[0]
|
||||
|
||||
data = data.replace("\n", "")
|
||||
|
||||
data = data.split(",")
|
||||
|
||||
w, h, c, dummy = map(int, data[0].strip("\"").split())
|
||||
|
||||
if w != 32 or h != 32 or c != 2:
|
||||
print("Skipping %r, expected 32x32, monochrome, got %s" %
|
||||
(xpm, data[0]))
|
||||
continue
|
||||
|
||||
# col_1 = data[1][1]
|
||||
col_2 = data[2][1]
|
||||
|
||||
data = [d[1:-1] for d in data[3:]]
|
||||
|
||||
bits = []
|
||||
|
||||
for d in data:
|
||||
for i, c in enumerate(d):
|
||||
bits.append('01'[(c == col_2)])
|
||||
|
||||
if len(bits) != 1024:
|
||||
print("Skipping %r, expected 1024 bits, got %d" %
|
||||
(xpm, len(bits)))
|
||||
continue
|
||||
|
||||
bits = "".join(bits)
|
||||
|
||||
chars = []
|
||||
|
||||
for i in range(0, len(bits), 8):
|
||||
chars.append("0x%.2x" % int(bits[i:i + 8], 2))
|
||||
|
||||
fout = sys.stdout
|
||||
|
||||
var = os.path.basename(xpm)
|
||||
var = os.path.splitext(var)[0]
|
||||
|
||||
fout.write("GLubyte stipple_%s[128] {\n\t" % var)
|
||||
for i, c in enumerate(chars):
|
||||
if i != 127:
|
||||
fout.write("%s, " % c)
|
||||
else:
|
||||
fout.write("%s" % c)
|
||||
|
||||
if not ((i + 1) % 8):
|
||||
fout.write("\n\t")
|
||||
fout.write("};\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
150
blender-5.2.0/tools/utils/make_shape_2d_from_blend.py
Normal file
150
blender-5.2.0/tools/utils/make_shape_2d_from_blend.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'''
|
||||
Created compact byte arrays which can be decoded into 2D shapes.
|
||||
(See 'GPU_batch_from_poly_2d_encoded').
|
||||
|
||||
- Objects must use the prefix "shape_"
|
||||
- Meshes and Curves are supported as input.
|
||||
- C and Python output is written to "output/"
|
||||
|
||||
The format is simple: a series of (X, Y) locations one byte each.
|
||||
Repeating the same value terminates the polygon, moving onto the next.
|
||||
|
||||
Example Use::
|
||||
|
||||
blender.bin -b --factory-startup my_shapes.blend --python make_shape_2d_from_blend.py
|
||||
'''
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import bpy
|
||||
import os
|
||||
|
||||
USE_C_STYLE = True
|
||||
USE_PY_STYLE = True
|
||||
|
||||
WRAP_LIMIT = 79
|
||||
TAB_WIDTH = 4
|
||||
|
||||
SUBDIR = "output"
|
||||
|
||||
|
||||
def float_to_ubyte(f):
|
||||
return max(0, min(255, int(round(f * 255.0))))
|
||||
|
||||
|
||||
def curve_to_loops(ob):
|
||||
import bmesh
|
||||
cu = ob.data
|
||||
|
||||
me = ob.to_mesh()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
me = ob.to_mesh_clear()
|
||||
|
||||
bmesh.ops.beautify_fill(bm, faces=bm.faces, edges=bm.edges)
|
||||
|
||||
edges = bm.edges[:]
|
||||
edges.sort(key=lambda e: e.calc_length(), reverse=True)
|
||||
|
||||
for e in edges:
|
||||
if e.is_manifold:
|
||||
f_a, f_b = [f for f in e.link_faces]
|
||||
bmesh.utils.face_join((f_a, f_b), False)
|
||||
|
||||
edges = bm.edges[:]
|
||||
for e in edges:
|
||||
if e.is_wire:
|
||||
bm.edges.remove(e)
|
||||
|
||||
bm.normal_update()
|
||||
|
||||
data_all = []
|
||||
for f in bm.faces:
|
||||
points = []
|
||||
# Ensure all faces are pointing the correct direction
|
||||
# Note, we may want to use polygon sign for a second color
|
||||
# (via the material index).
|
||||
if f.normal.z > 0.0:
|
||||
loops = f.loops
|
||||
else:
|
||||
loops = reversed(f.loops)
|
||||
for l in loops:
|
||||
points.append(
|
||||
tuple(float_to_ubyte(axis) for axis in l.vert.co.xy)
|
||||
)
|
||||
data_all.append((points, f.material_index))
|
||||
bm.free()
|
||||
return data_all
|
||||
|
||||
|
||||
def write_c(ob):
|
||||
cu = ob.data
|
||||
name = ob.name
|
||||
with open(os.path.join(SUBDIR, name + ".c"), 'w') as fh:
|
||||
fw = fh.write
|
||||
fw(f"/* {name} */\n")
|
||||
fw(f"const uchar {name}[] = {{")
|
||||
line_len = WRAP_LIMIT
|
||||
line_is_first = True
|
||||
array_len = 0
|
||||
data_all = curve_to_loops(ob)
|
||||
for (points, material_index) in data_all:
|
||||
# TODO, material_index
|
||||
for p in points + [points[-1]]:
|
||||
line_len += 12
|
||||
if line_len >= WRAP_LIMIT:
|
||||
fw("\n\t")
|
||||
line_len = TAB_WIDTH
|
||||
line_is_first = True
|
||||
if not line_is_first:
|
||||
fw(" ")
|
||||
fw(", ".join([f"0x{axis:02x}" for axis in p]) + ",")
|
||||
line_is_first = False
|
||||
array_len += (len(points) + 1) * 2
|
||||
fw("\n};\n")
|
||||
# fw(f"const int data_len = {array_len}\n")
|
||||
|
||||
|
||||
def write_py(ob):
|
||||
cu = ob.data
|
||||
name = ob.name
|
||||
with open(os.path.join(SUBDIR, name + ".py"), 'w') as fh:
|
||||
fw = fh.write
|
||||
fw(f"# {name}\n")
|
||||
fw("data = (")
|
||||
line_len = WRAP_LIMIT
|
||||
fw = fh.write
|
||||
data_all = curve_to_loops(ob)
|
||||
for (points, material_index) in data_all:
|
||||
# TODO, material_index
|
||||
for p in points + [points[-1]]:
|
||||
line_len += 8
|
||||
if line_len >= WRAP_LIMIT:
|
||||
if p is not points[0]:
|
||||
fw("'")
|
||||
fw("\n b'")
|
||||
line_len = 6
|
||||
fw("".join([f"\\x{axis:02x}" for axis in p]))
|
||||
fw("'\n)\n")
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(SUBDIR, exist_ok=True)
|
||||
for ob in bpy.data.objects:
|
||||
if ob.type not in {'MESH', 'CURVE'}:
|
||||
continue
|
||||
if not ob.name.startswith('shape_'):
|
||||
continue
|
||||
if USE_C_STYLE:
|
||||
write_c(ob)
|
||||
if USE_PY_STYLE:
|
||||
write_py(ob)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user