Add sequence pack operator and multi-blend-file CLI support

- Add --sequence CLI flag to pack multiple blend files together with
  shared dependency deduplication
- Add BAT_OT_sequence_pack Blender operator: scan a sequence folder for
  latest published blend files, review with checkboxes, pack to ZIP
- Add studio template system (Autour de Minuit, La Cabane Productions)
  with configurable folder conventions and task type selection
- Add BlenderProgressCallback for cursor progress indicator during pack
- Add ZIP write start/finish logging
- Deduplicate blend file paths in CLI when positional and --sequence overlap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 12:41:50 +01:00
co-authored by Claude Opus 4.6
parent f79c6a276d
commit 05d97cdeda
6 changed files with 441 additions and 81 deletions
+65 -29
View File
@@ -19,6 +19,7 @@
# (c) 2018, Blender Foundation - Sybren A. Stüvel
"""Create a BAT-pack for the given blend file."""
import logging
import os
import pathlib
import sys
import typing
@@ -34,7 +35,8 @@ def add_parser(subparsers):
parser = subparsers.add_parser("pack", help=__doc__)
parser.set_defaults(func=cli_pack)
parser.add_argument("blendfile", type=pathlib.Path, help="The Blend file to pack.")
parser.add_argument("blendfile", nargs='?', type=pathlib.Path, default=None,
help="The Blend file to pack (omit when using --sequence).")
parser.add_argument(
"target",
type=str,
@@ -97,12 +99,20 @@ def add_parser(subparsers):
"path structure under the target directory. Paths in blend files are "
"rewritten to relative paths within this structure.",
)
parser.add_argument(
"--sequence",
nargs="+",
type=pathlib.Path,
metavar="BLENDFILE",
help="Pack multiple blend files together, deduplicating shared dependencies. "
"All listed blend files and their dependencies are packed into the target.",
)
def cli_pack(args):
bpath, ppath, tpath = paths_from_cli(args)
bpaths, ppath, tpath = paths_from_cli(args)
with create_packer(args, bpath, ppath, tpath) as packer:
with create_packer(args, bpaths, ppath, tpath) as packer:
packer.strategise()
try:
packer.execute()
@@ -116,9 +126,12 @@ def cli_pack(args):
def create_packer(
args, bpath: pathlib.Path, ppath: pathlib.Path, target: str
args, bpaths: typing.List[pathlib.Path], ppath: pathlib.Path, target: str
) -> pack.Packer:
if target.startswith("s3:/"):
if len(bpaths) > 1:
raise ValueError("S3 uploader does not support --sequence")
if args.noop:
raise ValueError("S3 uploader does not support no-op.")
@@ -131,13 +144,16 @@ def create_packer(
if args.keep_hierarchy:
raise ValueError("S3 uploader does not support the --keep-hierarchy option")
packer = create_s3packer(bpath, ppath, pathlib.PurePosixPath(target))
packer = create_s3packer(bpaths[0], ppath, pathlib.PurePosixPath(target))
elif (
target.startswith("shaman+http:/")
or target.startswith("shaman+https:/")
or target.startswith("shaman:/")
):
if len(bpaths) > 1:
raise ValueError("Shaman uploader does not support --sequence")
if args.noop:
raise ValueError("Shaman uploader does not support no-op.")
@@ -154,7 +170,7 @@ def create_packer(
"Shaman uploader does not support the --keep-hierarchy option"
)
packer = create_shamanpacker(bpath, ppath, target)
packer = create_shamanpacker(bpaths[0], ppath, target)
elif target.lower().endswith(".zip"):
from blender_asset_tracer.pack import zipped
@@ -163,12 +179,12 @@ def create_packer(
raise ValueError("ZIP packer does not support on-the-fly compression")
packer = zipped.ZipPacker(
bpath, ppath, target, noop=args.noop, relative_only=args.relative_only,
bpaths, ppath, target, noop=args.noop, relative_only=args.relative_only,
keep_hierarchy=args.keep_hierarchy,
)
else:
packer = pack.Packer(
bpath,
bpaths,
ppath,
target,
noop=args.noop,
@@ -222,24 +238,42 @@ def create_shamanpacker(
)
def paths_from_cli(args) -> typing.Tuple[pathlib.Path, pathlib.Path, str]:
"""Return paths to blendfile, project, and pack target.
def paths_from_cli(args) -> typing.Tuple[typing.List[pathlib.Path], pathlib.Path, str]:
"""Return paths to blendfile(s), project, and pack target.
Calls sys.exit() if anything is wrong.
"""
bpath = args.blendfile
if not bpath.exists():
log.critical("File %s does not exist", bpath)
# Collect blend files from positional and --sequence arguments.
bpaths = [] # type: typing.List[pathlib.Path]
if args.blendfile is not None:
bpaths.append(args.blendfile)
if args.sequence:
bpaths.extend(args.sequence)
if not bpaths:
log.critical("No blend file specified. Provide a positional blendfile or use --sequence.")
sys.exit(3)
if bpath.is_dir():
log.critical("%s is a directory, should be a blend file")
sys.exit(3)
bpath = bpathlib.make_absolute(bpath)
# Deduplicate preserving order, in case the same file appears both as
# positional arg and in --sequence.
bpaths = list(dict.fromkeys(bpaths))
# Validate each blend file and make absolute.
for i, bpath in enumerate(bpaths):
if not bpath.exists():
log.critical("File %s does not exist", bpath)
sys.exit(3)
if bpath.is_dir():
log.critical("%s is a directory, should be a blend file", bpath)
sys.exit(3)
bpaths[i] = bpathlib.make_absolute(bpath)
tpath = args.target
if args.project is None:
ppath = bpathlib.make_absolute(bpath).parent
if len(bpaths) == 1:
ppath = bpaths[0].parent
else:
ppath = pathlib.Path(os.path.commonpath([p.parent for p in bpaths]))
log.warning("No project path given, using %s", ppath)
else:
ppath = bpathlib.make_absolute(args.project)
@@ -256,18 +290,20 @@ def paths_from_cli(args) -> typing.Tuple[pathlib.Path, pathlib.Path, str]:
)
ppath = ppath.parent
try:
bpath.relative_to(ppath)
except ValueError:
log.critical(
"Project directory %s does not contain blend file %s",
args.project,
bpath.absolute(),
)
sys.exit(5)
for bpath in bpaths:
try:
bpath.relative_to(ppath)
except ValueError:
log.critical(
"Project directory %s does not contain blend file %s",
ppath,
bpath,
)
sys.exit(5)
log.info("Blend file to pack: %s", bpath)
for bpath in bpaths:
log.info("Blend file to pack: %s", bpath)
log.info("Project path: %s", ppath)
log.info("Pack will be created in: %s", tpath)
return bpath, ppath, tpath
return bpaths, ppath, tpath