Last bit of merge fixes hopefully
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include "util/algorithm.h"
|
||||
#include "util/foreach.h"
|
||||
#include "util/set.h"
|
||||
#include <chrono>
|
||||
|
||||
CCL_NAMESPACE_BEGIN
|
||||
|
||||
|
||||
@@ -1116,12 +1116,7 @@ def _extensions_enabled_from_repo_directory_and_pkg_id_sequence(repo_directory_a
|
||||
return extensions_enabled_pending
|
||||
|
||||
|
||||
def _extensions_repo_sync_wheels(
|
||||
repo_cache_store, # `bl_extension_utils.RepoCacheStore`
|
||||
extensions_enabled, # `set[tuple[str, str]]`
|
||||
*,
|
||||
error_fn, # `Callable[[Exception], None]`
|
||||
): # `-> None`
|
||||
def _extensions_repo_sync_wheels(repo_cache_store, extensions_enabled):
|
||||
"""
|
||||
This function collects all wheels from all packages and ensures the packages are either extracted or removed
|
||||
when they are no longer used.
|
||||
@@ -1133,7 +1128,7 @@ def _extensions_repo_sync_wheels(
|
||||
wheel_list = []
|
||||
|
||||
for repo_index, pkg_manifest_local in enumerate(repo_cache_store.pkg_manifest_from_local_ensure(
|
||||
error_fn=error_fn,
|
||||
error_fn=print,
|
||||
ignore_missing=True,
|
||||
)):
|
||||
repo = repos_all[repo_index]
|
||||
@@ -1165,21 +1160,10 @@ def _extensions_repo_sync_wheels(
|
||||
)
|
||||
|
||||
|
||||
def _extensions_repo_refresh_on_change(
|
||||
repo_cache_store, # `bl_extension_utils.RepoCacheStore`
|
||||
*,
|
||||
extensions_enabled, # `set[tuple[str, str]] | None`
|
||||
compat_calc, # `bool`
|
||||
stats_calc, # `bool`
|
||||
error_fn, # `Callable[[Exception], None]`
|
||||
): # `-> None`
|
||||
def _extensions_repo_refresh_on_change(repo_cache_store, *, extensions_enabled, compat_calc, stats_calc):
|
||||
import addon_utils
|
||||
if extensions_enabled is not None:
|
||||
_extensions_repo_sync_wheels(
|
||||
repo_cache_store,
|
||||
extensions_enabled,
|
||||
error_fn=error_fn,
|
||||
)
|
||||
_extensions_repo_sync_wheels(repo_cache_store, extensions_enabled)
|
||||
# Wheel sync handled above.
|
||||
|
||||
if compat_calc:
|
||||
@@ -1789,10 +1773,7 @@ class EXTENSIONS_OT_repo_refresh_all(Operator):
|
||||
# In-line `bpy.ops.preferences.addon_refresh`.
|
||||
addon_utils.modules_refresh()
|
||||
# Ensure compatibility info and wheels is up to date.
|
||||
addon_utils.extensions_refresh(
|
||||
ensure_wheels=True,
|
||||
handle_error=lambda ex: self.report({'ERROR'}, str(ex)),
|
||||
)
|
||||
addon_utils.extensions_refresh(ensure_wheels=True)
|
||||
|
||||
_preferences_ui_redraw()
|
||||
_preferences_ui_refresh_addons()
|
||||
@@ -2259,10 +2240,6 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
|
||||
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
|
||||
del self.repo_lock
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
# Refresh installed packages for repositories that were operated on.
|
||||
repo_cache_store = repo_cache_store_ensure()
|
||||
for directory in self._repo_directories:
|
||||
@@ -2286,9 +2263,12 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled,
|
||||
compat_calc=True,
|
||||
stats_calc=True,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
for directory, pkg_id_sequence in self._repo_map_packages_addon_only:
|
||||
|
||||
pkg_manifest_local = repo_cache_store.refresh_local_from_directory(
|
||||
@@ -2317,7 +2297,6 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled_test,
|
||||
compat_calc=False,
|
||||
stats_calc=False,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
_preferences_ui_redraw()
|
||||
@@ -2441,7 +2420,6 @@ class EXTENSIONS_OT_package_uninstall_marked(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=_extensions_enabled(),
|
||||
compat_calc=True,
|
||||
stats_calc=True,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
_preferences_theme_state_restore(self._theme_restore)
|
||||
@@ -2644,10 +2622,6 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
|
||||
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
|
||||
del self.repo_lock
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
pkg_manifest_local = repo_cache_store.refresh_local_from_directory(
|
||||
directory=self.repo_directory,
|
||||
error_fn=self.error_fn_from_exception,
|
||||
@@ -2668,9 +2642,13 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled,
|
||||
compat_calc=True,
|
||||
stats_calc=True,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
_preferences_ensure_enabled_all(
|
||||
addon_restore=self._addon_restore,
|
||||
handle_error=handle_error,
|
||||
@@ -2700,7 +2678,6 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled_test,
|
||||
compat_calc=False,
|
||||
stats_calc=False,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
_extensions_repo_temp_files_make_stale(self.repo_directory)
|
||||
@@ -3017,10 +2994,6 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
|
||||
def exec_command_finish(self, canceled):
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
# Unlock repositories.
|
||||
lock_result_any_failed_with_report(self, self.repo_lock.release(), report_type='WARNING')
|
||||
del self.repo_lock
|
||||
@@ -3048,9 +3021,12 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled,
|
||||
compat_calc=True,
|
||||
stats_calc=True,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
# TODO: it would be nice to include this message in the banner.
|
||||
def handle_error(ex):
|
||||
self.report({'ERROR'}, str(ex))
|
||||
|
||||
_preferences_ensure_enabled_all(
|
||||
addon_restore=self._addon_restore,
|
||||
handle_error=handle_error,
|
||||
@@ -3080,7 +3056,6 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=extensions_enabled_test,
|
||||
compat_calc=False,
|
||||
stats_calc=False,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
_extensions_repo_temp_files_make_stale(self.repo_directory)
|
||||
@@ -3519,7 +3494,6 @@ class EXTENSIONS_OT_package_uninstall(Operator, _ExtCmdMixIn):
|
||||
extensions_enabled=_extensions_enabled(),
|
||||
compat_calc=True,
|
||||
stats_calc=True,
|
||||
error_fn=handle_error,
|
||||
)
|
||||
|
||||
_preferences_theme_state_restore(self._theme_restore)
|
||||
|
||||
@@ -222,6 +222,16 @@ def blender_ext_cmd(python_args: Sequence[str]) -> Sequence[str]:
|
||||
# Call JSON.
|
||||
#
|
||||
|
||||
def non_blocking_call(cmd: Sequence[str]) -> subprocess.Popen[bytes]:
|
||||
# pylint: disable-next=consider-using-with
|
||||
ps = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
stdout = ps.stdout
|
||||
assert stdout is not None
|
||||
# Needed so whatever is available can be read (without waiting).
|
||||
file_handle_make_non_blocking(stdout)
|
||||
return ps
|
||||
|
||||
|
||||
def command_output_from_json_0(
|
||||
args: Sequence[str],
|
||||
use_idle: bool,
|
||||
@@ -235,76 +245,57 @@ def command_output_from_json_0(
|
||||
chunk_list = []
|
||||
request_exit_signal_sent = False
|
||||
|
||||
# WIN32 needs to use a separate process-group else Blender will recieve the "break", see #131947.
|
||||
creationflags = 0
|
||||
if sys.platform == "win32":
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
while True:
|
||||
# It's possible this is multiple chunks.
|
||||
try:
|
||||
chunk = stdout.read()
|
||||
except Exception as ex:
|
||||
if not file_handle_non_blocking_is_error_blocking(ex):
|
||||
raise ex
|
||||
chunk = b''
|
||||
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=creationflags) as ps:
|
||||
stdout = ps.stdout
|
||||
assert stdout is not None
|
||||
json_messages = []
|
||||
|
||||
# Needed so whatever is available can be read (without waiting).
|
||||
file_handle_make_non_blocking(stdout)
|
||||
if not chunk:
|
||||
if ps.poll() is not None:
|
||||
break
|
||||
if use_idle:
|
||||
time.sleep(IDLE_WAIT_ON_READ)
|
||||
elif (chunk_zero_index := chunk.find(b'\0')) == -1:
|
||||
chunk_list.append(chunk)
|
||||
else:
|
||||
# Extract contiguous data from `chunk_list`.
|
||||
chunk_list.append(chunk[:chunk_zero_index])
|
||||
|
||||
chunk_list = []
|
||||
request_exit_signal_sent = False
|
||||
json_bytes_list = [b''.join(chunk_list)]
|
||||
chunk_list.clear()
|
||||
|
||||
while True:
|
||||
# It's possible this is multiple chunks.
|
||||
try:
|
||||
chunk = stdout.read()
|
||||
except Exception as ex:
|
||||
if not file_handle_non_blocking_is_error_blocking(ex):
|
||||
raise ex
|
||||
chunk = b''
|
||||
|
||||
json_messages = []
|
||||
|
||||
if not chunk:
|
||||
if ps.poll() is not None:
|
||||
break
|
||||
if use_idle:
|
||||
time.sleep(IDLE_WAIT_ON_READ)
|
||||
elif (chunk_zero_index := chunk.find(b'\0')) == -1:
|
||||
chunk_list.append(chunk)
|
||||
else:
|
||||
# Extract contiguous data from `chunk_list`.
|
||||
chunk_list.append(chunk[:chunk_zero_index])
|
||||
|
||||
json_bytes_list = [b''.join(chunk_list)]
|
||||
chunk_list.clear()
|
||||
|
||||
# There may be data afterwards, even whole chunks.
|
||||
if chunk_zero_index + 1 != len(chunk):
|
||||
# There may be data afterwards, even whole chunks.
|
||||
if chunk_zero_index + 1 != len(chunk):
|
||||
chunk = chunk[chunk_zero_index + 1:]
|
||||
# Add whole chunks.
|
||||
while (chunk_zero_index := chunk.find(b'\0')) != -1:
|
||||
json_bytes_list.append(chunk[:chunk_zero_index])
|
||||
chunk = chunk[chunk_zero_index + 1:]
|
||||
# Add whole chunks.
|
||||
while (chunk_zero_index := chunk.find(b'\0')) != -1:
|
||||
json_bytes_list.append(chunk[:chunk_zero_index])
|
||||
chunk = chunk[chunk_zero_index + 1:]
|
||||
if chunk:
|
||||
chunk_list.append(chunk)
|
||||
if chunk:
|
||||
chunk_list.append(chunk)
|
||||
|
||||
request_exit = False
|
||||
request_exit = False
|
||||
|
||||
for json_bytes in json_bytes_list:
|
||||
json_data = json.loads(json_bytes.decode("utf-8"))
|
||||
for json_bytes in json_bytes_list:
|
||||
json_data = json.loads(json_bytes.decode("utf-8"))
|
||||
|
||||
assert len(json_data) == 2
|
||||
assert isinstance(json_data[0], str)
|
||||
assert len(json_data) == 2
|
||||
assert isinstance(json_data[0], str)
|
||||
|
||||
json_messages.append((json_data[0], json_data[1]))
|
||||
json_messages.append((json_data[0], json_data[1]))
|
||||
|
||||
# Yield even when `json_messages`, otherwise this generator can block.
|
||||
# It also means a request to exit might not be responded to soon enough.
|
||||
request_exit = yield json_messages
|
||||
if request_exit and not request_exit_signal_sent:
|
||||
if sys.platform == "win32":
|
||||
# Caught by the `signal.SIGBREAK` signal handler.
|
||||
ps.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
ps.send_signal(signal.SIGINT)
|
||||
request_exit_signal_sent = True
|
||||
# Yield even when `json_messages`, otherwise this generator can block.
|
||||
# It also means a request to exit might not be responded to soon enough.
|
||||
request_exit = yield json_messages
|
||||
if request_exit and not request_exit_signal_sent:
|
||||
ps.send_signal(signal.SIGINT)
|
||||
request_exit_signal_sent = True
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1290,26 +1290,6 @@ class PathPatternMatch:
|
||||
# -----------------------------------------------------------------------------
|
||||
# URL Downloading
|
||||
|
||||
|
||||
# NOTE:
|
||||
# - Using return arguments isn't ideal but is better than including
|
||||
# a static value in the iterator.
|
||||
# - Other data could be added here as needed (response headers if the caller needs them).
|
||||
class DataRetrieveInfo:
|
||||
"""
|
||||
When accessing a file from a URL or from the file-system,
|
||||
this is a "return" argument so the caller can know the size of the chunks it's iterating over,
|
||||
or -1 when the size is not known.
|
||||
"""
|
||||
__slots__ = (
|
||||
"size_hint",
|
||||
)
|
||||
size_hint: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.size_hint = -1
|
||||
|
||||
|
||||
# Originally based on `urllib.request.urlretrieve`.
|
||||
def url_retrieve_to_data_iter(
|
||||
url: str,
|
||||
@@ -1320,13 +1300,19 @@ def url_retrieve_to_data_iter(
|
||||
timeout_in_seconds: float,
|
||||
) -> Generator[tuple[bytes, int, Any], None, None]:
|
||||
"""
|
||||
Iterate over byte data downloaded from a from a URL
|
||||
limited to ``chunk_size``.
|
||||
Retrieve a URL into a temporary location on disk.
|
||||
|
||||
- The ``retrieve_info.size_hint``
|
||||
will be set once the iterator starts and can be used for progress display.
|
||||
- The iterator will start with an empty block, so the size can be known
|
||||
before time is spent downloading data.
|
||||
Requires a URL argument. If a filename is passed, it is used as
|
||||
the temporary file location. The reporthook argument should be
|
||||
a callable that accepts a block number, a read size, and the
|
||||
total file size of the URL target. The data argument should be
|
||||
valid URL encoded data.
|
||||
|
||||
If a filename is passed and the URL points to a local resource,
|
||||
the result is a copy from local file to new file.
|
||||
|
||||
Returns a tuple containing the path to the newly created
|
||||
data file as well as the resulting HTTPMessage object.
|
||||
"""
|
||||
from urllib.error import ContentTooShortError
|
||||
from urllib.request import urlopen
|
||||
@@ -1348,10 +1334,7 @@ def url_retrieve_to_data_iter(
|
||||
if "content-length" in response_headers:
|
||||
size = int(response_headers["Content-Length"])
|
||||
|
||||
retrieve_info.size_hint = size
|
||||
|
||||
# Yield an empty block so progress display may start.
|
||||
yield b""
|
||||
yield (b'', size, response_headers)
|
||||
|
||||
if timeout_in_seconds <= 0.0:
|
||||
while True:
|
||||
@@ -1359,14 +1342,14 @@ def url_retrieve_to_data_iter(
|
||||
if not block:
|
||||
break
|
||||
read += len(block)
|
||||
yield block
|
||||
yield (block, size, response_headers)
|
||||
else:
|
||||
while True:
|
||||
block = read_with_timeout(fp, chunk_size, timeout_in_seconds=timeout_in_seconds)
|
||||
if not block:
|
||||
break
|
||||
read += len(block)
|
||||
yield block
|
||||
yield (block, size, response_headers)
|
||||
|
||||
if size >= 0 and read < size:
|
||||
raise ContentTooShortError(
|
||||
@@ -1375,7 +1358,6 @@ def url_retrieve_to_data_iter(
|
||||
)
|
||||
|
||||
|
||||
# See `url_retrieve_to_data_iter` doc-string.
|
||||
def url_retrieve_to_filepath_iter(
|
||||
url: str,
|
||||
filepath: str,
|
||||
@@ -1387,19 +1369,17 @@ def url_retrieve_to_filepath_iter(
|
||||
) -> Generator[tuple[int, int, Any], None, None]:
|
||||
# Handle temporary file setup.
|
||||
with open(filepath, 'wb') as fh_output:
|
||||
for block in url_retrieve_to_data_iter(
|
||||
for block, size, response_headers in url_retrieve_to_data_iter(
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
chunk_size=chunk_size,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=retrieve_info,
|
||||
):
|
||||
fh_output.write(block)
|
||||
yield len(block)
|
||||
yield (len(block), size, response_headers)
|
||||
|
||||
|
||||
# See `url_retrieve_to_data_iter` doc-string.
|
||||
def filepath_retrieve_to_filepath_iter(
|
||||
filepath_src: str,
|
||||
filepath: str,
|
||||
@@ -1411,12 +1391,11 @@ def filepath_retrieve_to_filepath_iter(
|
||||
# Handle temporary file setup.
|
||||
_ = timeout_in_seconds
|
||||
with open(filepath_src, 'rb') as fh_input:
|
||||
retrieve_info.size_hint = os.fstat(fh_input.fileno()).st_size
|
||||
yield 0
|
||||
size = os.fstat(fh_input.fileno()).st_size
|
||||
with open(filepath, 'wb') as fh_output:
|
||||
while (block := fh_input.read(chunk_size)):
|
||||
fh_output.write(block)
|
||||
yield len(block)
|
||||
yield (len(block), size)
|
||||
|
||||
|
||||
def url_retrieve_to_data_iter_or_filesystem(
|
||||
@@ -1424,25 +1403,25 @@ def url_retrieve_to_data_iter_or_filesystem(
|
||||
headers: dict[str, str],
|
||||
chunk_size: int,
|
||||
timeout_in_seconds: float,
|
||||
retrieve_info: DataRetrieveInfo,
|
||||
) -> Iterator[bytes]:
|
||||
) -> Generator[bytes, None, None]:
|
||||
if url_is_filesystem(url):
|
||||
with open(path_from_url(url), "rb") as fh_source:
|
||||
retrieve_info.size_hint = os.fstat(fh_source.fileno()).st_size
|
||||
yield b""
|
||||
while (block := fh_source.read(chunk_size)):
|
||||
yield block
|
||||
else:
|
||||
yield from url_retrieve_to_data_iter(
|
||||
for (
|
||||
block,
|
||||
_size,
|
||||
_response_headers,
|
||||
) in url_retrieve_to_data_iter(
|
||||
url,
|
||||
headers=headers,
|
||||
chunk_size=chunk_size,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=retrieve_info,
|
||||
)
|
||||
):
|
||||
yield block
|
||||
|
||||
|
||||
# See `url_retrieve_to_data_iter` doc-string.
|
||||
def url_retrieve_to_filepath_iter_or_filesystem(
|
||||
url: str,
|
||||
filepath: str,
|
||||
@@ -1460,17 +1439,16 @@ def url_retrieve_to_filepath_iter_or_filesystem(
|
||||
filepath,
|
||||
chunk_size=chunk_size,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=retrieve_info,
|
||||
)
|
||||
else:
|
||||
yield from url_retrieve_to_filepath_iter(
|
||||
for (read, size, _response_headers) in url_retrieve_to_filepath_iter(
|
||||
url,
|
||||
filepath,
|
||||
headers=headers,
|
||||
chunk_size=chunk_size,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=retrieve_info,
|
||||
)
|
||||
):
|
||||
yield (read, size)
|
||||
|
||||
|
||||
def url_retrieve_exception_is_connectivity(
|
||||
@@ -2987,9 +2965,8 @@ def repo_sync_from_remote(
|
||||
return False
|
||||
|
||||
try:
|
||||
retrieve_info = DataRetrieveInfo()
|
||||
read_total = 0
|
||||
for read in url_retrieve_to_filepath_iter_or_filesystem(
|
||||
for (read, size) in url_retrieve_to_filepath_iter_or_filesystem(
|
||||
remote_json_url,
|
||||
local_json_path_temp,
|
||||
headers=url_request_headers_create(
|
||||
@@ -2999,14 +2976,12 @@ def repo_sync_from_remote(
|
||||
),
|
||||
chunk_size=CHUNK_SIZE_DEFAULT,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=retrieve_info,
|
||||
):
|
||||
request_exit |= msglog.progress("Downloading...", read_total, retrieve_info.size_hint, 'BYTE')
|
||||
request_exit |= msglog.progress("Downloading...", read_total, size, 'BYTE')
|
||||
if request_exit:
|
||||
break
|
||||
read_total += read
|
||||
del read_total
|
||||
del retrieve_info
|
||||
except (Exception, KeyboardInterrupt) as ex:
|
||||
msg = url_retrieve_exception_as_message(ex, prefix="sync", url=remote_url)
|
||||
if demote_connection_errors_to_status and url_retrieve_exception_is_connectivity(ex):
|
||||
@@ -3913,7 +3888,6 @@ class subcmd_client:
|
||||
),
|
||||
chunk_size=CHUNK_SIZE_DEFAULT,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=DataRetrieveInfo(), # Unused.
|
||||
):
|
||||
result.write(block)
|
||||
|
||||
@@ -4365,7 +4339,6 @@ class subcmd_client:
|
||||
),
|
||||
chunk_size=CHUNK_SIZE_DEFAULT,
|
||||
timeout_in_seconds=timeout_in_seconds,
|
||||
retrieve_info=DataRetrieveInfo(), # Unused.
|
||||
):
|
||||
request_exit |= msglog.progress(
|
||||
"Downloading \"{:s}\"".format(pkg_idname),
|
||||
|
||||
@@ -1233,15 +1233,13 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
|
||||
num_loops = len(me.loops)
|
||||
t_ln = np.empty(num_loops * 3, dtype=normal_bl_dtype)
|
||||
# t_lnw = np.zeros(len(me.loops), dtype=np.float64)
|
||||
# WARNING: Since tangent layers are recomputed inside the loop, do not directly iterate over the
|
||||
# uvlayers. Instead, cache their keys (names), and use this cached data inside the loop to compute
|
||||
# the tangent layers.
|
||||
uvlayer_names = [uvl.name for uvl in me.uv_layers]
|
||||
for idx, name in enumerate(uvlayer_names):
|
||||
# Annoying, `me.calc_tangent` errors in case there is no geometry...
|
||||
if num_loops > 0:
|
||||
uv_names = [uvlayer.name for uvlayer in me.uv_layers]
|
||||
# Annoying, `me.calc_tangent` errors in case there is no geometry...
|
||||
if num_loops > 0:
|
||||
for name in uv_names:
|
||||
me.calc_tangents(uvmap=name)
|
||||
|
||||
for idx, uvlayer in enumerate(me.uv_layers):
|
||||
name = uvlayer.name
|
||||
# Loop bitangents (aka binormals).
|
||||
# NOTE: this is not supported by importer currently.
|
||||
me.loops.foreach_get("bitangent", t_ln)
|
||||
|
||||
@@ -144,7 +144,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
|
||||
target_property = fcurve.data_path
|
||||
target_data = targets_extra.get(target, {})
|
||||
target_data['type'] = type_
|
||||
target_data['bone'] = target.name if type_ == "BONE" else None
|
||||
target_data['bone'] = target.name
|
||||
target_data['obj_uuid'] = obj_uuid
|
||||
target_properties = target_data.get('properties', {})
|
||||
channels = target_properties.get(target_property, [])
|
||||
|
||||
@@ -44,9 +44,6 @@ class BlenderAnimation():
|
||||
BlenderPointerAnim.anim(gltf, anim_idx, light, light_idx, 'LIGHT')
|
||||
|
||||
for mat_idx, mat in enumerate(gltf.data.materials if gltf.data.materials else []):
|
||||
if len(mat.blender_material) == 0:
|
||||
# The animated material is not used in Blender, so do not animate it
|
||||
continue
|
||||
if len(mat.animations) != 0:
|
||||
BlenderPointerAnim.anim(gltf, anim_idx, mat, mat_idx, 'MATERIAL')
|
||||
if mat.normal_texture is not None and len(mat.normal_texture.animations) != 0:
|
||||
@@ -160,8 +157,5 @@ class BlenderAnimation():
|
||||
restore_animation_on_object(light['blender_object_data'], animation_name)
|
||||
|
||||
for mat in gltf.data.materials if gltf.data.materials else []:
|
||||
if len(mat.blender_material) == 0:
|
||||
# The animated material is not used in Blender, so do not animate it
|
||||
continue
|
||||
restore_animation_on_object(mat.blender_nodetree, animation_name)
|
||||
restore_animation_on_object(mat.blender_mat, animation_name)
|
||||
|
||||
@@ -203,15 +203,6 @@ class PoseActionCreator:
|
||||
"""Resolve an RNA path + array index to an actual value."""
|
||||
value_or_array = cls._path_resolve(datablock, data_path)
|
||||
|
||||
if isinstance(value_or_array, str):
|
||||
# Enums resolve to a string.
|
||||
bone_path, enum_property_name = data_path.rsplit("[", 1)
|
||||
unescaped_property_name = bpy.utils.unescape_identifier(enum_property_name)
|
||||
# unescaped_property_name still has the quotes and a bracket at the end hence the [1:-2].
|
||||
value = cls._path_resolve(datablock, bone_path).get(unescaped_property_name[1:-2])
|
||||
assert isinstance(value, (int, float))
|
||||
return cast(FCurveValue, value)
|
||||
|
||||
# Both indices -1 and 0 are used for non-array properties.
|
||||
# -1 cannot be used in arrays, whereas 0 can be used in both arrays and non-arrays.
|
||||
|
||||
|
||||
@@ -241,12 +241,8 @@ class VIEW3D_PT_vr_info(bpy.types.Panel):
|
||||
return not bpy.app.build_options.xr_openxr
|
||||
|
||||
def draw(self, context):
|
||||
import platform
|
||||
layout = self.layout
|
||||
missing_support_string = "Built without VR/OpenXR features"
|
||||
if platform.system() == "Darwin":
|
||||
missing_support_string = "VR is not supported on macOS at the moment"
|
||||
layout.label(icon='ERROR', text=missing_support_string)
|
||||
layout.label(icon='ERROR', text="Built without VR/OpenXR features")
|
||||
|
||||
|
||||
classes = (
|
||||
|
||||
@@ -13,7 +13,6 @@ __all__ = (
|
||||
"apply_action",
|
||||
)
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -180,9 +179,6 @@ def _zipfile_extractall_safe(
|
||||
zip_fh: zipfile.ZipFile,
|
||||
path: str,
|
||||
path_restrict: str,
|
||||
*,
|
||||
error_fn: Callable[[Exception], None],
|
||||
remove_error_fn: Callable[[str, Exception], None],
|
||||
) -> None:
|
||||
"""
|
||||
A version of ``ZipFile.extractall`` that wont write to paths outside ``path_restrict``.
|
||||
@@ -207,69 +203,20 @@ def _zipfile_extractall_safe(
|
||||
path_restrict_with_slash = path_restrict + sep
|
||||
assert len(path) >= len(path_restrict_with_slash)
|
||||
if not path.startswith(path_restrict_with_slash):
|
||||
# This is an internal error if it ever happens.
|
||||
raise Exception("Expected the restricted directory to start with \"{:s}\"".format(path_restrict_with_slash))
|
||||
|
||||
has_error = False
|
||||
member_index = 0
|
||||
|
||||
# Use an iterator to avoid duplicating the checks (for the cleanup pass).
|
||||
def zip_iter_filtered(*, verbose: bool) -> Iterator[tuple[zipfile.ZipInfo, str, str]]:
|
||||
for member in zip_fh.infolist():
|
||||
filename_orig = member.filename
|
||||
filename_next = path_prefix + filename_orig
|
||||
|
||||
# This isn't likely to happen so accept a noisy print here.
|
||||
# If this ends up happening more often, it could be suppressed.
|
||||
# (although this hints at bigger problems because we might be excluding necessary files).
|
||||
if os.path.normpath(filename_next).startswith(".." + sep):
|
||||
if verbose:
|
||||
print("Skipping path:", filename_next, "that escapes:", path_restrict)
|
||||
continue
|
||||
yield member, filename_orig, filename_next
|
||||
|
||||
for member, filename_orig, filename_next in zip_iter_filtered(verbose=True):
|
||||
# Increment before extracting, so a potential cleanup will a file that failed to extract.
|
||||
member_index += 1
|
||||
|
||||
member.filename = filename_next
|
||||
|
||||
# Extraction can fail for many reasons, see: #132924.
|
||||
try:
|
||||
zip_fh.extract(member, path_restrict)
|
||||
except Exception as ex:
|
||||
error_fn(ex)
|
||||
|
||||
filepath_native = path_restrict + sep + filename_next.replace("/", sep)
|
||||
print("Failed to extract path:", filepath_native, "error", str(ex))
|
||||
remove_error_fn(filepath_native, ex)
|
||||
has_error = True
|
||||
raise Exception("Expected the restricted directory to start with ")
|
||||
|
||||
for member in zip_fh.infolist():
|
||||
filename_orig = member.filename
|
||||
member.filename = path_prefix + filename_orig
|
||||
# This isn't likely to happen so accept a noisy print here.
|
||||
# If this ends up happening more often, it could be suppressed.
|
||||
# (although this hints at bigger problems because we might be excluding necessary files).
|
||||
if os.path.normpath(member.filename).startswith(".." + sep):
|
||||
print("Skipping path:", member.filename, "that escapes:", path_restrict)
|
||||
continue
|
||||
zip_fh.extract(member, path_restrict)
|
||||
member.filename = filename_orig
|
||||
|
||||
if has_error:
|
||||
break
|
||||
|
||||
# If the zip-file failed to extract, remove all files that were extracted.
|
||||
# This is done so failure to extract a file never results in a partially-working
|
||||
# state which can cause confusing situations for users.
|
||||
if has_error:
|
||||
# NOTE: this currently leaves empty directories which is not ideal.
|
||||
# It's possible to calculate directories created by this extraction but more involved.
|
||||
member_cleanup_len = member_index + 1
|
||||
member_index = 0
|
||||
|
||||
for member, filename_orig, filename_next in zip_iter_filtered(verbose=False):
|
||||
member_index += 1
|
||||
if member_index >= member_cleanup_len:
|
||||
break
|
||||
|
||||
filepath_native = path_restrict + sep + filename_next.replace("/", sep)
|
||||
try:
|
||||
os.unlink(filepath_native)
|
||||
except Exception as ex:
|
||||
remove_error_fn(filepath_native, ex)
|
||||
|
||||
|
||||
WHEEL_VERSION_RE = re.compile(r"(\d+)?(?:\.(\d+))?(?:\.(\d+))")
|
||||
|
||||
@@ -472,20 +419,5 @@ def apply_action(
|
||||
filepath = wheels_dir_info_to_filepath_map[dir_info]
|
||||
# `ZipFile.extractall` is needed because some wheels contain paths that point to parent directories.
|
||||
# Handle this *safely* by allowing extracting to parent directories but limit this to the `local_dir`.
|
||||
|
||||
try:
|
||||
# pylint: disable-next=consider-using-with
|
||||
zip_fh_context = zipfile.ZipFile(filepath, mode="r")
|
||||
except Exception as ex:
|
||||
print("Error ({:s}) opening zip-file: {:s}".format(str(ex), filepath))
|
||||
error_fn(ex)
|
||||
continue
|
||||
|
||||
with contextlib.closing(zip_fh_context) as zip_fh:
|
||||
_zipfile_extractall_safe(
|
||||
zip_fh,
|
||||
local_dir_site_packages,
|
||||
local_dir,
|
||||
error_fn=error_fn,
|
||||
remove_error_fn=remove_error_fn,
|
||||
)
|
||||
with zipfile.ZipFile(filepath, mode="r") as zip_fh:
|
||||
_zipfile_extractall_safe(zip_fh, local_dir_site_packages, local_dir)
|
||||
|
||||
@@ -1212,10 +1212,6 @@ def _initialize_extensions_compat_data(
|
||||
|
||||
if ensure_wheels:
|
||||
if updated:
|
||||
if error_fn is None:
|
||||
def error_fn(ex):
|
||||
print("Error:", str(ex))
|
||||
|
||||
try:
|
||||
_initialize_extensions_compat_ensure_up_to_date_wheels(extensions_directory, wheel_list, debug)
|
||||
except Exception:
|
||||
|
||||
@@ -4848,6 +4848,7 @@ def km_weight_paint(params):
|
||||
("wm.context_toggle", {"type": 'S', "value": 'PRESS', "shift": True},
|
||||
{"properties": [("data_path", "tool_settings.weight_paint.brush.use_smooth_stroke")]}),
|
||||
op_menu_pie("VIEW3D_MT_wpaint_vgroup_lock_pie", {"type": 'K', "value": 'PRESS'}),
|
||||
("paint.face_vert_reveal", {"type": 'H', "value": 'PRESS', "alt": True}, None),
|
||||
*_template_items_context_panel("VIEW3D_PT_paint_weight_context_menu", params.context_menu_event),
|
||||
op_asset_shelf_popup(
|
||||
"VIEW3D_AST_brush_weight_paint",
|
||||
|
||||
@@ -7,25 +7,15 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
exe_dir, exe_file = os.path.split(sys.executable)
|
||||
is_python = exe_file.startswith("python")
|
||||
|
||||
# Path to Blender shared libraries.
|
||||
shared_lib_dirname = "blender.shared" if sys.platform == "win32" else "lib"
|
||||
|
||||
if os.path.basename(__file__) == "bpy_site_customize.py":
|
||||
# Blender as Python Module.
|
||||
is_python = True
|
||||
shared_lib_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
# On Windows no subdirectory is used.
|
||||
if sys.platform != "win32":
|
||||
shared_lib_dir = os.path.join(shared_lib_dir, shared_lib_dirname)
|
||||
if is_python:
|
||||
shared_lib_dir = os.path.abspath(os.path.join(exe_dir, "..", "..", "..", shared_lib_dirname))
|
||||
else:
|
||||
exe_dir, exe_file = os.path.split(sys.executable)
|
||||
is_python = exe_file.startswith("python")
|
||||
if is_python:
|
||||
# Python executable bundled with Blender.
|
||||
shared_lib_dir = os.path.abspath(os.path.join(exe_dir, "..", "..", "..", shared_lib_dirname))
|
||||
else:
|
||||
# Blender executable.
|
||||
shared_lib_dir = os.path.abspath(os.path.join(exe_dir, shared_lib_dirname))
|
||||
shared_lib_dir = os.path.abspath(os.path.join(exe_dir, shared_lib_dirname))
|
||||
|
||||
if sys.platform == "win32":
|
||||
# Directory for extensions to find DLLs.
|
||||
|
||||
@@ -6900,6 +6900,7 @@ class VIEW3D_PT_overlay_object(Panel):
|
||||
|
||||
sub = split.column(align=True)
|
||||
sub.prop(overlay, "show_bones", text="Bones")
|
||||
sub.prop(overlay, "isolate_bones", text="Isolate Bones")
|
||||
sub.prop(overlay, "show_motion_paths")
|
||||
sub.prop(overlay, "show_object_origins", text="Origins")
|
||||
subsub = sub.column()
|
||||
|
||||
@@ -1265,7 +1265,6 @@ elseif(WIN32)
|
||||
install(
|
||||
FILES
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python${_PYTHON_VERSION_NO_DOTS}.dll
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python3.dll
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python.exe
|
||||
DESTINATION ${BLENDER_VERSION}/python/bin
|
||||
CONFIGURATIONS Release;RelWithDebInfo;MinSizeRel
|
||||
@@ -1273,7 +1272,6 @@ elseif(WIN32)
|
||||
install(
|
||||
FILES
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python${_PYTHON_VERSION_NO_DOTS}_d.dll
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python3_d.dll
|
||||
${LIBDIR}/python/${_PYTHON_VERSION_NO_DOTS}/bin/python_d.exe
|
||||
DESTINATION ${BLENDER_VERSION}/python/bin
|
||||
CONFIGURATIONS Debug
|
||||
@@ -1770,7 +1768,7 @@ install(
|
||||
# -----------------------------------------------------------------------------
|
||||
# Bundle assets
|
||||
|
||||
set(ASSET_BUNDLE_DIR ${CMAKE_SOURCE_DIR}/assets/)
|
||||
set(ASSET_BUNDLE_DIR ${CMAKE_SOURCE_DIR}/release/datafiles/assets/publish/)
|
||||
|
||||
if(EXISTS "${ASSET_BUNDLE_DIR}")
|
||||
install(
|
||||
|
||||
@@ -2484,11 +2484,8 @@ static bool handle_load_file(bContext *C, const char *filepath_arg, const bool l
|
||||
/* Load the file. */
|
||||
ReportList reports;
|
||||
BKE_reports_init(&reports, RPT_PRINT);
|
||||
/* When activating from the command line there isn't an exact equivalent to operator properties.
|
||||
* Instead, enabling auto-execution via `--enable-autoexec` causes the auto-execution
|
||||
* check to be skipped (if it's set), so it's fine to always enable the check here. */
|
||||
const bool use_scripts_autoexec_check = true;
|
||||
const bool success = WM_file_read(C, filepath, use_scripts_autoexec_check, &reports);
|
||||
WM_file_autoexec_init(filepath);
|
||||
const bool success = WM_file_read(C, filepath, &reports);
|
||||
BKE_reports_free(&reports);
|
||||
|
||||
if (success) {
|
||||
|
||||
@@ -753,6 +753,17 @@ if(WITH_CYCLES OR WITH_GPU_RENDER_TESTS)
|
||||
list(APPEND _gpu_render_tests_arguments --fail-silently)
|
||||
endif()
|
||||
|
||||
# Eevee
|
||||
foreach(render_test ${gpu_render_tests})
|
||||
add_render_test(
|
||||
eevee_${render_test}
|
||||
${CMAKE_CURRENT_LIST_DIR}/eevee_render_tests.py
|
||||
-testdir "${TEST_SRC_DIR}/render/${render_test}"
|
||||
-outdir "${TEST_OUT_DIR}/eevee"
|
||||
${_gpu_render_tests_arguments}
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# Eevee Next
|
||||
if(WITH_OPENGL_BACKEND)
|
||||
foreach(render_test ${gpu_render_tests})
|
||||
|
||||
Reference in New Issue
Block a user