Start of dependency tracer work.

This commit is contained in:
Sybren A. Stüvel
2018-02-26 18:16:15 +01:00
parent 59c0b6df4c
commit 15cd74cda4
12 changed files with 340 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Blender path support.
Does not use pathlib, because we may have to handle POSIX paths on Windows
or vice versa.
"""
import os.path
import string
class BlendPath(bytes):
"""A path within Blender is always stored as bytes."""
def __new__(cls, path):
if isinstance(path, str):
# As a convenience, when a string is given, interpret as UTF-8.
return bytes.__new__(cls, path.encode('utf-8'))
return bytes.__new__(cls, path)
def __str__(self) -> str:
"""Decodes the path as UTF-8, replacing undecodable bytes.
Undecodable bytes are ignored so this function can be safely used
for reporting.
"""
return self.decode('utf8', errors='replace')
def is_blendfile_relative(self) -> bool:
return self[:2] == b'//'
def is_absolute(self) -> bool:
if self.is_blendfile_relative():
return False
if self[0:1] == b'/':
return True
# Windows style path starting with drive letter.
if (len(self) >= 3 and
(self.decode('utf8'))[0] in string.ascii_letters and
self[1:2] == b':' and
self[2:3] in {b'\\', b'/'}):
return True
return False
def absolute(self, root: bytes=None) -> 'BlendPath':
"""Determine absolute path.
:param root: root directory to compute paths relative to.
For blendfile-relative paths, root should be the directory
containing the blendfile. If not given, blendfile-relative
paths cause a ValueError but filesystem-relative paths are
resolved based on the current working directory.
"""
if self.is_absolute():
return self
if self.is_blendfile_relative():
my_relpath = self[2:] # strip off leading //
else:
my_relpath = self
return BlendPath(os.path.join(root, my_relpath))
+37
View File
@@ -0,0 +1,37 @@
import logging
import pathlib
import typing
from blender_asset_tracer import bpathlib, blendfile
from . import result, blocks
log = logging.getLogger(__name__)
codes_to_skip = {
b'LI', # Library blocks we handle after the blend file itself.
b'ID', b'WM', b'SN', # These blocks never have external assets.
}
def deps(bfilepath: pathlib.Path) -> typing.Iterator[result.BlockUsage]:
log.info('Tracing %s', bfilepath)
with blendfile.BlendFile(bfilepath) as bfile:
for block in asset_holding_blocks(bfile):
yield from blocks.from_block(block)
# TODO: handle library blocks for recursion.
def asset_holding_blocks(bfile: blendfile.BlendFile) -> typing.Iterator[blendfile.BlendFileBlock]:
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
+49
View File
@@ -0,0 +1,49 @@
import logging
import sys
import typing
from blender_asset_tracer import blendfile
from . import result, cdefs
log = logging.getLogger(__name__)
_warned_about_types = set()
class NoReaderImplemented(NotImplementedError):
"""There is no reader implementation for a specific block code."""
def __init__(self, message: str, code: bytes):
super().__init__(message)
self.code = code
def from_block(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
assert block.code != b'DATA'
module = sys.modules[__name__]
funcname = '_from_block_' + block.code.decode().lower()
try:
block_reader = getattr(module, funcname)
except AttributeError:
if block.code not in _warned_about_types:
log.warning('No reader implemented for block type %r', block.code.decode())
_warned_about_types.add(block.code)
return
yield from block_reader(block)
def _from_block_im(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
# old files miss this
image_source = block.get(b'source', default=cdefs.IMA_SRC_FILE)
if image_source not in {cdefs.IMA_SRC_FILE, cdefs.IMA_SRC_SEQUENCE, cdefs.IMA_SRC_MOVIE}:
return
if block[b'packedfile']:
return
pathname, field = block.get(b'name', return_field=True)
# TODO: the receiver should inspect the 'source' property too, and if set
# to cdefs.IMA_SRC_SEQUENCE yield the entire sequence of files.
yield result.BlockUsage(block, field, pathname)
+47
View File
@@ -0,0 +1,47 @@
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
"""Constants defined in C."""
# DNA_sequence_types.h (Sequence.type)
SEQ_TYPE_IMAGE = 0
SEQ_TYPE_META = 1
SEQ_TYPE_SCENE = 2
SEQ_TYPE_MOVIE = 3
SEQ_TYPE_SOUND_RAM = 4
SEQ_TYPE_SOUND_HD = 5
SEQ_TYPE_MOVIECLIP = 6
SEQ_TYPE_MASK = 7
SEQ_TYPE_EFFECT = 8
IMA_SRC_FILE = 1
IMA_SRC_SEQUENCE = 2
IMA_SRC_MOVIE = 3
# DNA_modifier_types.h
eModifierType_MeshCache = 46
# DNA_particle_types.h
PART_DRAW_OB = 7
PART_DRAW_GR = 8
# DNA_object_types.h
# Object.transflag
OB_DUPLIGROUP = 1 << 8
CACHE_LIBRARY_SOURCE_CACHE = 1
+35
View File
@@ -0,0 +1,35 @@
class BlockUsage:
"""Represents the use of an asset by a data block.
:ivar block:
:type block: blendfile.BlendFileBlock
:ivar field:
:type field: dna.Field
:ivar asset_path:
:type asset_path: bpathlib.BlendPath
"""
def __init__(self, block, field, asset_path):
self.block_idname = block[b'id', b'name']
from blender_asset_tracer import blendfile, bpathlib
from blender_asset_tracer.blendfile import dna
assert isinstance(block, blendfile.BlendFileBlock)
assert isinstance(field, dna.Field), 'field should be dna.Field, not %r' % type(field)
assert isinstance(asset_path, (bytes, bpathlib.BlendPath)), \
'asset_path should be BlendPath, not %r' % type(asset_path)
if isinstance(asset_path, bytes):
asset_path = bpathlib.BlendPath(asset_path)
self.block = block
self.field = field
self.asset_path = asset_path
def __repr__(self):
return '<BlockUsage name=%r type=%r field=%r asset=%r>' % (
self.block_idname, self.block.dna_type_name,
self.field.name.name_full.decode(), self.asset_path)