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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user