File size: 5,650 Bytes
cd53438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""SimReady asset packaging — entry point for the /simready-package skill.

Usage:
    python package.py <target_dir> [--profile NAME] [--output DIR]

For each immediate subfolder of <target_dir> containing a USD file, picks
the interface USD and runs `simready ingest usd` on it, producing a
packaged tree under <output_dir>. Independent of /simready-report — runs
the packaging step alone, without validation or dashboard generation.
"""
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path

USD_EXTS = (".usd", ".usda", ".usdc", ".usdz")


def _find_simready_cli() -> str | None:
    """Locate the `simready` CLI in a layout-agnostic way.

    Order: (1) on PATH, (2) alongside `sys.executable` (covers any active
    venv/conda regardless of OS).
    """
    on_path = shutil.which("simready")
    if on_path:
        return on_path
    py_bin = Path(sys.executable).parent
    for candidate in (py_bin / "simready.exe", py_bin / "simready"):
        if candidate.is_file():
            return str(candidate)
    return None


def _discover_interface_usds(target: Path) -> list[Path]:
    """Pick one interface USD per immediate subfolder of `target`.

    Heuristic per subfolder:
      - If exactly one USD at root, that's the interface.
      - Else prefer the USD whose stem matches the folder name (case- and
        hyphen/underscore-insensitive).
      - Else fall back to packaging all of them.
    Subfolders starting with "." or "_" are ignored.
    """
    interface_files: list[Path] = []
    for sub in sorted(target.iterdir()):
        if not sub.is_dir() or sub.name.startswith((".", "_")):
            continue
        usds = [p for ext in USD_EXTS for p in sub.glob(f"*{ext}")]
        if not usds:
            continue
        if len(usds) == 1:
            interface_files.append(usds[0])
            continue
        named = [p for p in usds if p.stem.lower() == sub.name.lower().replace("-", "_")]
        if named:
            interface_files.append(named[0])
        else:
            interface_files.extend(usds)
    return interface_files


def _default_output(target: Path) -> Path:
    """Pick a default output dir based on target location."""
    target_str = str(target).replace("\\", "/")
    if "/assets_to_validate/" in target_str:
        # Workspace-style layout: <X>/assets_to_validate/<name>
        # → output at sibling packages dir: <X>/packages/<name>
        return target.parent.parent / "packages" / target.name
    return target.parent / "packages" / target.name


def main() -> int:
    ap = argparse.ArgumentParser(description="Package SimReady assets via `simready ingest usd`.")
    ap.add_argument("target", nargs="?", default=None,
                    help="Directory containing one or more asset subfolders (default: cwd)")
    ap.add_argument("--profile", default="Robot-Body-Runnable",
                    help="Profile passed to `simready ingest usd -p` (default: Robot-Body-Runnable)")
    ap.add_argument("--output", default=None,
                    help="Output dir (default: <target>.parent/packages/<target.name>, "
                         "or sibling-of-assets_to_validate/packages/<name> when applicable)")
    args = ap.parse_args()

    target = Path(args.target).resolve() if args.target else Path.cwd().resolve()
    if not target.is_dir():
        print(f"ERROR: target is not a directory: {target}", flush=True)
        return 2

    simready_exe = _find_simready_cli()
    if simready_exe is None:
        print("ERROR: `simready` CLI not found on PATH or alongside the active Python.", flush=True)
        print("       Install simready-oem-sdk-poc into the active Python, or run bootstrap.ps1.", flush=True)
        return 2

    interface_files = _discover_interface_usds(target)
    if not interface_files:
        print(f"ERROR: no USD files found in immediate subfolders of {target}.", flush=True)
        print("       /simready-package expects each asset to live in its own subfolder "
              "with an interface USD.", flush=True)
        return 2

    output = Path(args.output).resolve() if args.output else _default_output(target)
    output.mkdir(parents=True, exist_ok=True)

    print(f"Target:  {target}", flush=True)
    print(f"Output:  {output}", flush=True)
    print(f"Profile: {args.profile}", flush=True)
    print(f"CLI:     {simready_exe}", flush=True)
    print(f"Discovered {len(interface_files)} interface USD(s).", flush=True)

    env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"}
    packaged = 0
    skipped = 0
    failures: list[tuple[str, str]] = []

    for usd in interface_files:
        already = output / usd.stem / usd.name
        if already.is_file():
            print(f"  skip (already packaged): {usd.name}", flush=True)
            skipped += 1
            continue
        print(f"  package: {usd.name}", flush=True)
        cmd = [simready_exe, "ingest", "usd", str(usd),
               "-o", str(output), "-p", args.profile, "--no-validate"]
        try:
            subprocess.run(cmd, env=env, check=True, capture_output=True, text=True, timeout=120)
            packaged += 1
        except subprocess.CalledProcessError as e:
            err = (e.stderr or "")[-300:] or str(e)
            failures.append((usd.name, err))
            print(f"  FAILED: {usd.name} - {err}", flush=True)

    print(f"\nSUMMARY: packaged={packaged} skipped={skipped} failed={len(failures)}", flush=True)
    print(f"OUTPUT: {output}", flush=True)
    return 0 if not failures else 1


if __name__ == "__main__":
    sys.exit(main())