more indirect detection work

This commit is contained in:
Nathan
2025-08-22 17:07:28 -06:00
parent 439682ae65
commit faaf4c8879
2 changed files with 144 additions and 30 deletions

View File

@@ -16232,3 +16232,54 @@ Try it:
--- ---
_**User**_
that relocator works fantastically. now we just need to be able to detect indirect links that are missing, make their parent library blend status indirect, and we're golden. the indirect status should only apply to library blends that have an indirectly linked blend that is missing.
---
_**Assistant**_
<think><details><summary>Thought Process</summary>**Exploring Blender libraries**
Im thinking about how to use Blenders `bpy.data.libraries` to list not just the direct libraries in a scene but also the indirect ones. Normally, these indirect libraries arent loaded in the main file, but after relocation or reload, Blender can have instances of both. In the `outliner`, the indirectly linked libraries show up under "Libraries." I want to make a safe algorithm to build a set of unique library filepaths, covering both direct and indirect libraries. This could be really useful!
**Marking indirect libraries**
I need to mark a library item as 'indirect' only if it has a missing indirectly linked dependency. They mentioned that if a top-level library L is directly linked in the main file but internally links to a missing library M, I should mark L as indirect, sort of like a warning. Earlier, they wanted missing libraries to be highlighted in red, but now they're focusing only on those missing indirect dependencies. I wonder if I can detect this by trying to open the library file path with `bpy.data.libraries.load`. Its a bit risky since deleted data might cause issues, but using `bpy.types.BlendDataLibraries` could help with loading.
**Detecting indirect libraries**
I'm looking into using `bpy.data.libraries.load` as a context manager that might give sets of datablocks and possibly a `lazy` option. In Blender 4.5, asset reading is a thing, but earlier there was data loss. Alternatively, I could run `bpy.ops.file.find_missing_files()`, although it seems heavy for the system. A better option might be scanning `bpy.data.libraries` to find libraries not directly used by any ID. There's a property `Library.indirect`, and I recall attributes of `bpy.types.Library` like `filepath`, `version`, and `id`. I could also check the `ID.is_library_indirect` property on datablocks to detect indirect links for libraries.
**Marking indirect dependencies**
They want to mark a parent as indirect if it has missing indirect dependencies. I need to scan all IDs where `is_library_indirect` is true and collect their source library file paths, then map them to the direct parent library referencing them. But which parent? Its the direct library that links to the indirect one. In the current file, objects referencing an indirect library wont have their library property equal to L. This means the main scene shouldn't reference M directly — only L's internal IDs should reference M. So, I cant use `is_library_indirect` on the scene's IDs to detect this. Instead, I should try to reload libraries and catch any errors to identify missing indirect libraries.
**Detecting indirect library dependencies**
I want to figure out how to mark a parent as indirect when it has missing indirect dependencies. A heuristic I think could work is to scan all IDs with `is_library_indirect` set to true and collect their source library file paths. Then, I need to detect if any direct libraries link to these indirectly linked libraries. I might attempt to reload direct libraries to find any missing dependencies.
To keep it efficient, I'll implement a lightweight binary parser to read the .blend file headers for 'LI' blocks. This way, I can avoid heavy scans that have caused crashes before. During the `find_libraries_in_folders` operation, I can determine which direct library references a missing library, helping me mark the relevant parent libraries as indirect if they have unresolved dependencies.
**Implementing indirect library detection**
I'm thinking of a pragmatic strategy to detect and mark indirect libraries as missing. Using `bpy.ops.outliner.lib_operation(type='RELOCATE')` seems complex, so I might analyze console errors like "Cannot find lib 'path'" to identify which parent library references a missing library. If a relocation attempt fails, I can set a flag on that parent library item to mark it as indirect.
Additionally, I can implement a new operator that reads the .blend file for library paths to identify missing references without altering the scene. I'll limit the read size for efficiency and avoid previous crashes. Once done, the UI can update to reflect any indirect statuses correctly. I'll adjust the code in `operators.py` to support this logic.</details></think>
---
---
---
- I added a context-free relocate button that lets you pick a new .blend and reloads that specific library. Great to hear it works well.
- Ive now implemented missing-indirect detection per your rule:
- A library is marked INDIRECT only if it appears to reference at least one missing indirectly linked .blend.
- Implementation scans each librarys .blend bytes for plausible .blend paths and checks existence; relative paths (//) are ignored; any missing absolute .blend reference marks the parent as INDIRECT.
- Sorting and UI remain the same; only the parents `is_indirect` flag is set when a missing indirect child is detected.
---

View File

@@ -101,6 +101,76 @@ class DLM_OT_scan_linked_assets(Operator):
return "Unknown" return "Unknown"
return os.path.basename(filepath) return os.path.basename(filepath)
# Helper: naive scan of a .blend file to find referenced library paths
def scan_blend_for_missing_indirects(blend_path: str) -> bool:
"""Return True if the .blend likely references at least one missing
library (.blend) path. This uses a conservative byte-string scan that
never loads data into Blender and is thus context-free and safe.
Strategy: look for substrings ending in .blend inside the file bytes
and expand left/right to the nearest NUL byte or line break to
reconstruct a plausible filesystem path. If any such path does not
exist on disk, we consider it an indirect missing dependency.
"""
try:
if not blend_path or not os.path.exists(blend_path):
return False
with open(blend_path, 'rb') as f:
data = f.read()
except Exception:
return False
missing_found = False
needle = b'.blend'
start = 0
max_hits = 100 # safety cap
hits = 0
data_len = len(data)
while True:
idx = data.find(needle, start)
if idx == -1:
break
hits += 1
if hits > max_hits:
break
# expand left to a separator or NUL
left = idx
while left > 0 and data[left-1] not in (0, 10, 13): # not NUL/\n/\r
# allow typical path chars; stop on unlikely byte
if data[left-1] in b'\t':
break
left -= 1
# keep paths bounded
if idx - left > 512:
break
# expand right a little (after .blend there should be NUL/space)
right = idx + len(needle)
while right < data_len and data[right] not in (0, 10, 13):
if data[right] in b' \t':
break
right += 1
if right - idx > 512:
break
try:
raw = data[left:right]
# decode best-effort
s = raw.decode('utf-8', errors='ignore')
# heuristic: must contain a slash or backslash to be a path
if ('/' in s) or ('\\' in s):
# trim any trailing quotes/spaces
s = s.strip().strip('"\'')
# relative paths are considered unknown; skip
if s.startswith('//'):
pass
else:
if not os.path.exists(s):
missing_found = True
break
except Exception:
pass
start = idx + len(needle)
return missing_found
# Scan all data collections for linked items # Scan all data collections for linked items
all_libraries = set() all_libraries = set()
@@ -144,38 +214,31 @@ class DLM_OT_scan_linked_assets(Operator):
# Store results in scene properties # Store results in scene properties
context.scene.dynamic_link_manager.linked_assets_count = len(all_libraries) context.scene.dynamic_link_manager.linked_assets_count = len(all_libraries)
# Create library items for the UI with better indirect detection # Create library items for the UI, and compute parent-indirect status
# Rule: A library is flagged 'indirect' ONLY if it has at least one
# missing indirect dependency.
library_items = [] library_items = []
for filepath in sorted(all_libraries): for filepath in sorted(all_libraries):
if filepath: if not filepath:
lib_item = context.scene.dynamic_link_manager.linked_libraries.add() continue
lib_item.filepath = filepath lib_item = context.scene.dynamic_link_manager.linked_libraries.add()
lib_item.name = get_library_name(filepath) lib_item.filepath = filepath
lib_item.is_missing = is_file_missing(filepath) lib_item.name = get_library_name(filepath)
lib_item.is_missing = is_file_missing(filepath)
# Better indirect link detection
# A library is indirect if it's linked from within another linked library # Determine if this library should be marked indirect because it
is_indirect = False # appears to reference at least one missing child library.
is_indirect_due_to_missing_child = False
# Check if this library filepath appears in any other library's linked datablocks try:
for other_lib in all_libraries: abspath = bpy.path.abspath(filepath) if filepath else filepath
if other_lib != filepath: # Don't check against self is_indirect_due_to_missing_child = scan_blend_for_missing_indirects(abspath)
# Look for datablocks that are linked from this other library except Exception:
for obj in bpy.data.objects: is_indirect_due_to_missing_child = False
if (hasattr(obj, 'library') and obj.library and
obj.library.filepath == other_lib and lib_item.is_indirect = bool(is_indirect_due_to_missing_child)
hasattr(obj, 'data') and obj.data and
hasattr(obj.data, 'library') and obj.data.library and # Store for sorting
obj.data.library.filepath == filepath): library_items.append((lib_item, filepath))
is_indirect = True
break
if is_indirect:
break
lib_item.is_indirect = is_indirect
# Store for sorting
library_items.append((lib_item, filepath))
# Sort libraries: missing first, then by name # Sort libraries: missing first, then by name
library_items.sort(key=lambda x: (not x[0].is_missing, get_library_name(x[1]).lower())) library_items.sort(key=lambda x: (not x[0].is_missing, get_library_name(x[1]).lower()))