Merge branch 'blender-v4.2-release'

This commit is contained in:
Campbell Barton 2024-07-04 12:41:23 +10:00
commit a9763ffabc
2 changed files with 226 additions and 2 deletions

@ -834,6 +834,12 @@ def cli_extension_args_extra(subparsers: "argparse._SubParsersAction[argparse.Ar
def cli_extension_handler(args: List[str]) -> int:
from .cli import blender_ext
# Override the default valid tags with a file which Blender includes.
blender_ext.ARG_DEFAULTS_OVERRIDE.build_valid_tags = os.path.join(
os.path.dirname(__file__), "..", "..", "modules", "_bpy_internal", "extensions", "tags.py",
)
result = blender_ext.main(
args,
args_internal=False,

@ -122,6 +122,20 @@ ${body}
</html>
'''
class _ArgsDefaultOverride:
__slots__ = (
"build_valid_tags",
)
def __init__(self) -> None:
self.build_valid_tags = ""
# Support overriding this value so Blender can default to a different tags file.
ARG_DEFAULTS_OVERRIDE = _ArgsDefaultOverride()
del _ArgsDefaultOverride
# Standard out may be communicating with a parent process,
# arbitrary prints are NOT acceptable.
@ -208,6 +222,12 @@ def force_exit_ok_enable() -> None:
# -----------------------------------------------------------------------------
# Generic Functions
def execfile(filepath: str) -> Dict[str, Any]:
global_namespace = {"__file__": filepath, "__name__": "__main__"}
with open(filepath, "rb") as fh:
exec(compile(fh.read(), filepath, 'exec'), global_namespace)
return global_namespace
def size_as_fmt_string(num: float, *, precision: int = 1) -> str:
for unit in ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"):
@ -1317,6 +1337,84 @@ def pkg_manifest_validate_terse_description_or_error(value: str) -> Optional[str
return None
# -----------------------------------------------------------------------------
# Manifest Validation (Tags)
def pkg_manifest_tags_load_valid_map_from_python(
valid_tags_filepath: str,
) -> Union[str, Dict[str, Set[str]]]:
try:
data = execfile(valid_tags_filepath)
except Exception as ex:
return "Python evaluation error ({:s})".format(str(ex))
result = {}
for key, key_extension_type in (("addons", "add-on"), ("themes", "theme")):
if (value := data.get(key)) is None:
return "missing key \"{:s}\"".format(key)
if not isinstance(value, set):
return "key \"{:s}\" must be a set, not a {:s}".format(key, str(type(value)))
for tag in value:
if not isinstance(tag, str):
return "key \"{:s}\" must contain strings, found a {:s}".format(key, str(type(tag)))
result[key_extension_type] = value
return result
def pkg_manifest_tags_load_valid_map_from_json(
valid_tags_filepath: str,
) -> Union[str, Dict[str, Set[str]]]:
try:
with open(valid_tags_filepath, "rb") as fh:
data = json.load(fh)
except Exception as ex:
return "JSON evaluation error ({:s})".format(str(ex))
if not isinstance(data, dict):
return "JSON must contain a dict not a {:s}".format(str(type(data)))
result = {}
for key in ("add-on", "theme"):
if (value := data.get(key)) is None:
return "missing key \"{:s}\"".format(key)
if not isinstance(value, list):
return "key \"{:s}\" must be a list, not a {:s}".format(key, str(type(value)))
for tag in value:
if not isinstance(tag, str):
return "key \"{:s}\" must contain strings, found a {:s}".format(key, str(type(tag)))
result[key] = set(value)
return result
def pkg_manifest_tags_load_valid_map(
valid_tags_filepath: str,
) -> Union[str, Dict[str, Set[str]]]:
# Allow Python data (Blender stores this internally).
if valid_tags_filepath.endswith(".py"):
return pkg_manifest_tags_load_valid_map_from_python(valid_tags_filepath)
return pkg_manifest_tags_load_valid_map_from_json(valid_tags_filepath)
def pkg_manifest_tags_valid_or_error(
valid_tags_data: Dict[str, Any],
manifest_type: str,
manifest_tags: List[str],
) -> Optional[str]:
valid_tags = valid_tags_data[manifest_type]
for tag in manifest_tags:
if tag not in valid_tags:
return (
"found invalid tag \"{:s}\" not found in:\n"
"({:s})"
).format(tag, ", ".join(sorted(valid_tags)))
return None
# -----------------------------------------------------------------------------
# Manifest Validation (Generic Callbacks)
#
@ -2427,6 +2525,48 @@ def generic_arg_build_split_platforms(subparse: argparse.ArgumentParser) -> None
)
def generic_arg_package_valid_tags(subparse: argparse.ArgumentParser) -> None:
# NOTE(@ideasman42): when called from Blender tags for `extensions.blender.org` are enforced by default.
# For `extensions.blender.org` this is enforced on the server side, so it's better developers see the error
# on build/validate instead of uploading the package.
# It's worth noting not all extensions will be hosted on `extensions.blender.org`,
# 3rd party hosting should remain a first class citizen not some exceptional case.
#
# The rationale for applying these tags for all packages even accepting that not everyone is targeting
# Blender's official repository is to avoid every extension defining their own tags.
#
# This has two down sides:
# - Duplicate similar tags, e.g. `"render", "rendering"`, `"toon", "cartoon"` etc.
# - Tag proliferation (100's of tags), makes the UI unusable.
# So even when all tags are valid and named well, having everyone defining
# their own tags results the user having to filter between too many options.
# Although a re-designed UI could account for this if it were important.
#
# Nevertheless, allow motivated developers to ignore the tags limitations as it's somewhat arbitrarily.
# The default to apply these limits is a "nudge" to avoid additional tags from typos as well as a hint
# that tags should be added to Blender's list if they're needed instead of being defined ad-hoc.
subparse.add_argument(
"--valid-tags",
dest="valid_tags_filepath",
default=ARG_DEFAULTS_OVERRIDE.build_valid_tags,
metavar="VALID_TAGS_JSON",
# NOTE(@ideasman42): Python input is also supported, intentionally undocumented for now,
# since this is only supported as Blender's tags happen to be stored as a Python script - which may change.
help=(
"Reference a file path containing valid tags lists.\n"
"\n"
"If you wish to reference custom tags a ``.json`` file can be used.\n"
"The contents must be a dictionary of lists where the ``key`` matches the extension type.\n"
"\n"
"For example:\n"
" ``{\"add-ons\": [\"Example\", \"Another\"], \"theme\": [\"Other\", \"Tags\"]}``\n"
"\n"
"To disable validating tags, pass in an empty path ``--valid-tags=\"\"``."
),
)
# -----------------------------------------------------------------------------
# Argument Handlers ("server-generate" command)
@ -3548,6 +3688,7 @@ class subcmd_author:
pkg_output_dir: str,
pkg_output_filepath: str,
split_platforms: bool,
valid_tags_filepath: str,
verbose: bool,
) -> bool:
if not os.path.isdir(pkg_source_dir):
@ -3591,6 +3732,15 @@ class subcmd_author:
)
return False
if valid_tags_filepath:
if subcmd_author._validate_tags(
msg_fn,
manifest=manifest,
pkg_manifest_filepath=pkg_manifest_filepath,
valid_tags_filepath=valid_tags_filepath,
) is False:
return False
if (manifest_build_data := manifest_data.get("build")) is not None:
if "generated" in manifest_build_data:
message_error(
@ -3803,11 +3953,42 @@ class subcmd_author:
message_status(msg_fn, "created: \"{:s}\", {:d}".format(outfile, os.path.getsize(outfile)))
return True
@staticmethod
def _validate_tags(
msg_fn: MessageFn,
*,
manifest: PkgManifest,
# NOTE: This path is only for inclusion in the error message,
# the path may not exist on the file-system (it may refer to a path inside an archive for e.g.).
pkg_manifest_filepath: str,
valid_tags_filepath: str,
) -> bool:
assert valid_tags_filepath
if manifest.tags is not None:
if isinstance(valid_tags_data := pkg_manifest_tags_load_valid_map(valid_tags_filepath), str):
message_error(
msg_fn,
"Error in TAGS \"{:s}\" loading tags: {:s}".format(valid_tags_filepath, valid_tags_data),
)
return False
if (error := pkg_manifest_tags_valid_or_error(valid_tags_data, manifest.type, manifest.tags)) is not None:
message_error(
msg_fn,
(
"Error in TOML \"{:s}\" loading tags: {:s}\n"
"Either correct the tag or disable validation using an empty tags argument --valid-tags=\"\", "
"see --help text for details."
).format(pkg_manifest_filepath, error),
)
return False
return True
@staticmethod
def _validate_directory(
msg_fn: MessageFn,
*,
pkg_source_dir: str,
valid_tags_filepath: str,
) -> bool:
pkg_manifest_filepath = os.path.join(pkg_source_dir, PKG_MANIFEST_FILENAME_TOML)
@ -3837,6 +4018,15 @@ class subcmd_author:
if not ok:
return False
if valid_tags_filepath:
if subcmd_author._validate_tags(
msg_fn,
manifest=manifest,
pkg_manifest_filepath=pkg_manifest_filepath,
valid_tags_filepath=valid_tags_filepath,
) is False:
return False
message_status(msg_fn, "Success parsing TOML in \"{:s}\"".format(pkg_source_dir))
return True
@ -3845,6 +4035,7 @@ class subcmd_author:
msg_fn: MessageFn,
*,
pkg_source_archive: str,
valid_tags_filepath: str,
) -> bool:
# NOTE(@ideasman42): having `_validate_directory` & `_validate_archive`
# use separate code-paths isn't ideal in some respects however currently the difference
@ -3877,6 +4068,19 @@ class subcmd_author:
message_status(msg_fn, error_msg)
return False
if valid_tags_filepath:
if subcmd_author._validate_tags(
msg_fn,
manifest=manifest,
# Only for the error message, use the ZIP relative path.
pkg_manifest_filepath=(
"{:s}/{:s}".format(archive_subdir, PKG_MANIFEST_FILENAME_TOML) if archive_subdir else
PKG_MANIFEST_FILENAME_TOML
),
valid_tags_filepath=valid_tags_filepath,
) is False:
return False
# NOTE: this is arguably *not* manifest validation, the check could be refactored out.
# Currently we always want to check both and it's useful to do that while the informatio
expected_files = []
@ -3905,11 +4109,20 @@ class subcmd_author:
msg_fn: MessageFn,
*,
source_path: str,
valid_tags_filepath: str,
) -> bool:
if os.path.isdir(source_path):
result = subcmd_author._validate_directory(msg_fn, pkg_source_dir=source_path)
result = subcmd_author._validate_directory(
msg_fn,
pkg_source_dir=source_path,
valid_tags_filepath=valid_tags_filepath,
)
else:
result = subcmd_author._validate_archive(msg_fn, pkg_source_archive=source_path)
result = subcmd_author._validate_archive(
msg_fn,
pkg_source_archive=source_path,
valid_tags_filepath=valid_tags_filepath,
)
return result
@ -4002,6 +4215,7 @@ def unregister():
pkg_output_dir=repo_dir,
pkg_output_filepath="",
split_platforms=False,
valid_tags_filepath="",
verbose=False,
):
# Error running command.
@ -4251,6 +4465,7 @@ def argparse_create_author_build(
generic_arg_package_source_dir(subparse)
generic_arg_package_output_dir(subparse)
generic_arg_package_output_filepath(subparse)
generic_arg_package_valid_tags(subparse)
generic_arg_build_split_platforms(subparse)
generic_arg_verbose(subparse)
@ -4264,6 +4479,7 @@ def argparse_create_author_build(
pkg_output_dir=args.output_dir,
pkg_output_filepath=args.output_filepath,
split_platforms=args.split_platforms,
valid_tags_filepath=args.valid_tags_filepath,
verbose=args.verbose,
),
)
@ -4280,6 +4496,7 @@ def argparse_create_author_validate(
formatter_class=argparse.RawTextHelpFormatter,
)
generic_arg_package_source_path_positional(subparse)
generic_arg_package_valid_tags(subparse)
if args_internal:
generic_arg_output_type(subparse)
@ -4288,6 +4505,7 @@ def argparse_create_author_validate(
func=lambda args: subcmd_author.validate(
msg_fn_from_args(args),
source_path=args.source_path,
valid_tags_filepath=args.valid_tags_filepath,
),
)