2018-03-02 10:06:41 +01:00

102 lines
3.5 KiB
Python

import logging
import pathlib
import typing
from blender_asset_tracer import blendfile, bpathlib
from . import result, blocks2assets
log = logging.getLogger(__name__)
codes_to_skip = {
# These blocks never have external assets:
b'ID', b'WM', b'SN',
# These blocks are skipped for now, until we have proof they point to
# assets otherwise missed:
b'GR', b'WO', b'BR', b'LS',
}
class _Tracer:
"""Trace dependencies with protection against infinite loops.
Don't use this directly, use the function deps(...) instead.
"""
def __init__(self):
self.seen_files = set()
def deps(self, bfilepath: pathlib.Path, recursive=False) -> typing.Iterator[result.BlockUsage]:
"""Open the blend file and report its dependencies.
:param bfilepath: File to open.
:param recursive: Also report dependencies inside linked blend files.
"""
log.info('Tracing %s', bfilepath)
bfilepath = bfilepath.absolute().resolve()
self.seen_files.add(bfilepath)
recurse_into = []
with blendfile.BlendFile(bfilepath) as bfile:
for block in asset_holding_blocks(bfile):
yield from blocks2assets.iter_assets(block)
if recursive and block.code == b'LI':
recurse_into.append(block)
# Deal with recursion after we've handled all dependencies of the
# current file, so that file access isn't interleaved and all deps
# of one file are reported before moving to the next.
for block in recurse_into:
yield from self._recurse_deps(block)
def _recurse_deps(self, lib_block: blendfile.BlendFileBlock) \
-> typing.Iterator[result.BlockUsage]:
"""Call deps() on the file linked from the library block."""
if lib_block.code != b'LI':
raise ValueError('Expected LI block, not %r' % lib_block)
relpath = bpathlib.BlendPath(lib_block[b'name'])
abspath = lib_block.bfile.abspath(relpath)
# Convert bytes to pathlib.Path object so we have a nice interface to work with.
# This assumes the path is encoded in UTF-8.
path = pathlib.Path(abspath.decode())
try:
path = path.resolve()
except FileNotFoundError:
log.warning('Linked blend file %s (%s) does not exist; skipping.', relpath, path)
return
# Avoid infinite recursion.
if path in self.seen_files:
log.debug('ignoring file, already seen %s', path)
return
yield from self.deps(path, recursive=True)
def deps(bfilepath: pathlib.Path, recursive=False) -> typing.Iterator[result.BlockUsage]:
"""Open the blend file and report its dependencies.
:param bfilepath: File to open.
:param recursive: Also report dependencies inside linked blend files.
"""
tracer = _Tracer()
yield from tracer.deps(bfilepath, recursive=recursive)
def asset_holding_blocks(bfile: blendfile.BlendFile) -> typing.Iterator[blendfile.BlendFileBlock]:
"""Generator, yield data blocks that could reference external assets."""
for block in bfile.blocks:
assert isinstance(block, blendfile.BlendFileBlock)
code = block.code
# The longer codes are either arbitrary data or data blocks that
# don't refer to external assets. The former data blocks will be
# visited when we hit the two-letter datablocks that use them.
if len(code) > 2 or code in codes_to_skip:
continue
yield block