diff --git a/.github/workflows/build-usd-exchange.yml b/.github/workflows/build-usd-exchange.yml index 254c022d1832..65779316cf25 100644 --- a/.github/workflows/build-usd-exchange.yml +++ b/.github/workflows/build-usd-exchange.yml @@ -150,7 +150,7 @@ jobs: cmake --install /work/_build/cmake --config Release --prefix /work/_build/install "$PY" - <<'STAGE_EOF' - import glob, importlib.metadata, os, shutil, sys, toml + import glob, importlib.metadata, os, shutil, subprocess, sys, toml usd ="/work/_build/usd" usdex = "/work/_build/install" @@ -188,9 +188,6 @@ jobs: else: sys.exit(f"plugin {plugin} not found") copy(f"{usd}/plugin/usd/usdShaders.so", f"{libs}/usd/usdShaders.so") - for name in validators: - if name.endswith("Validators"): - copy(f"{usd}/lib/libusd_{name}.so", f"{libs}/libusd_{name}.so") mtlx = f"{libs}/usd/usdMtlx/resources/libraries" for src in glob.glob(f"{usd}/libraries/bxdf/*open_pbr_surface.mtlx"): @@ -206,6 +203,26 @@ jobs: if not glob.glob(f"{staging}/pxr/{module}/{lib}*.so"): sys.exit(f"pxr.{module} has no {lib} binding") + # Stage every native library beside the plugInfo.json files above, under its own SONAME rather than + # auditwheel's content-hash name: usd-exchange 3.0.1's check_wheel_imports.py requires the exact SONAME, + # matching upstream's own staging (tools/repoman/py_package.py's __stage_linux_libraries). + # build_usd.py's own pxr install always uses "lib" (it never calls GNUInstallDirs), but the --onetbb + # dependency it builds does: oneTBB's CMakeLists calls include(GNUInstallDirs) and never overrides + # CMAKE_INSTALL_LIBDIR, so on this Rocky Linux 10 image (64-bit, no /etc/debian_version) GNUInstallDirs' + # own default resolves to "lib64" - libtbb.so.12 lands in {usd}/lib64, not {usd}/lib. + for source_lib in (f"{usd}/lib", f"{usd}/lib64", f"{usdex}/lib"): + for lib in sorted(glob.glob(f"{source_lib}/*.so*")): + if os.path.islink(lib): + continue + with open(lib, "rb") as f: + if f.read(4) != b"\x7fELF": + continue + soname = subprocess.run(["patchelf", "--print-soname", lib], capture_output=True, text=True, check=True).stdout.strip() + if soname: + copy(lib, f"{libs}/{soname}") + with open("/work/_build/staged_sonames.txt", "w") as f: + f.writelines(f"{name}\n" for name in sorted(os.listdir(libs)) if os.path.isfile(f"{libs}/{name}")) + notices = { "materialx-LICENSE.txt": glob.glob("/work/_build/usd-src/MaterialX-*/LICENSE")[0], "onetbb-LICENSE.txt": glob.glob("/work/_build/usd-src/oneTBB-*/LICENSE.txt")[0], @@ -240,37 +257,36 @@ jobs: find /work/_build/pyproject /work/_build/usd/lib /work/_build/install/lib -name '*.so*' -type f \ -exec sh -c 'head -c4 "$1" | grep -q ELF && strip "$1"' _ {} \; + # Point every staged binary's RPATH at usd_exchange.libs so the libraries staged above resolve without + # auditwheel re-grafting (and content-hash renaming) them. + find /work/_build/pyproject -name '*.so*' -type f | while read -r binary; do + head -c4 "$binary" | grep -q ELF || continue + relative="$(realpath --relative-to="$(dirname "$binary")" /work/_build/pyproject/usd_exchange.libs)" + if [ "$relative" = "." ]; then + rpath="\$ORIGIN" + else + rpath="\$ORIGIN/${relative}" + fi + patchelf --force-rpath --set-rpath "$rpath" "$binary" + done + "$PY" -m build --wheel --outdir /work/_build/pyproject/dist /work/_build/pyproject - LD_LIBRARY_PATH="/work/_build/install/lib:/work/_build/usd/lib:${LD_LIBRARY_PATH:-}" \ - auditwheel repair -w /work/_build/wheelhouse /work/_build/pyproject/dist/*.whl - - "$PY" - <<'PLUGINFO_EOF' - import glob, json, os, re, subprocess, sys, tempfile - - wheel = glob.glob("/work/_build/wheelhouse/*.whl")[0] - with tempfile.TemporaryDirectory() as tmp: - subprocess.run([sys.executable, "-m", "wheel", "unpack", wheel, "--dest", tmp], check=True) - unpacked = glob.glob(f"{tmp}/*/")[0].rstrip("/") - libs_root = f"{unpacked}/usd_exchange.libs" - hashed = {} - for lib in glob.glob(f"{libs_root}/*.so*"): - match = re.match(r"^(lib.+?)-[0-9a-f]{6,}\.so", os.path.basename(lib)) - if match: - hashed[match.group(1)] = os.path.basename(lib) - for plug_info in glob.glob(f"{libs_root}/usd/*/resources/plugInfo.json"): - with open(plug_info) as f: - data = json.loads("".join(line for line in f if not line.lstrip().startswith("#"))) - modified = False - for plug in data.get("Plugins", []): - lib = hashed.get(f"libusd_{plug.get('Name')}") - if lib and "LibraryPath" in plug: - plug["LibraryPath"] = f"../../{lib}" - modified = True - if modified: - with open(plug_info, "w") as f: - json.dump(data, f, indent=4) - subprocess.run([sys.executable, "-m", "wheel", "pack", unpacked, "--dest-dir", "/work/dist"], check=True) - PLUGINFO_EOF + + # Exclude the libraries staged (and rpath'd) above, so auditwheel leaves their SONAMEs alone instead of + # grafting a second, content-hash-renamed copy; a resolution miss then fails the wheel tests instead. + # This image's statically-linked cp3xx also pulls in libcrypt, which is on no manylinux policy's + # lib_whitelist (any architecture); trust the target's libxcrypt instead of vendoring it, the same way + # other workflows here trust a target's libssl/libcrypto. + exclude_args=() + while read -r soname; do + exclude_args+=(--exclude "$soname") + done < <(cat /work/_build/staged_sonames.txt; find /work/_build/pyproject -name '*.so*' -type f \ + -exec sh -c 'head -c4 "$1" | grep -q ELF && readelf -d --wide "$1"' _ {} \; \ + | awk '/\(NEEDED\)/ {gsub(/[][]/, "", $NF); print $NF}' | grep '^libcrypt\.so' | sort -u) + LD_LIBRARY_PATH="/work/_build/install/lib:/work/_build/usd/lib:/work/_build/usd/lib64:${LD_LIBRARY_PATH:-}" \ + auditwheel repair -w /work/_build/wheelhouse "${exclude_args[@]}" /work/_build/pyproject/dist/*.whl + + cp /work/_build/wheelhouse/*.whl /work/dist/ ls -la /work/dist USDEX_BUILD_EOF diff --git a/docs/packages/usd-exchange.yaml b/docs/packages/usd-exchange.yaml index ebf850434933..034d9cf5e906 100644 --- a/docs/packages/usd-exchange.yaml +++ b/docs/packages/usd-exchange.yaml @@ -18,3 +18,4 @@ versions: - filename: usd_exchange-3.0.0-cp313-cp313-manylinux_2_39_riscv64.whl sha256: 1237b9c7d3579c59f97a139458c8841d1712583a01966a3265b2f07e03b5ba38 requires-python: <3.14,>=3.10 +- version: 3.0.1 diff --git a/patches/usd-exchange/3.0.1/0001-Support-riscv64-in-pxr-base-arch.patch b/patches/usd-exchange/3.0.1/0001-Support-riscv64-in-pxr-base-arch.patch new file mode 100644 index 000000000000..a022f2e8a1cc --- /dev/null +++ b/patches/usd-exchange/3.0.1/0001-Support-riscv64-in-pxr-base-arch.patch @@ -0,0 +1,121 @@ +From e346d5ee838ae79333ed9e0f6242891c841ed1b8 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 21 Sep 2026 01:53:09 +0000 +Subject: [PATCH] Support riscv64 in pxr/base/arch + +pxr/base/arch/defines.h recognises only x86 and ARM, and its "Bits" block +ends in #error "Unsupported architecture. x86_64 or ARM64 required." - so +every translation unit of USD fails on riscv64 before anything else is +compiled. Two further sites are reached once that is fixed: arch/math.h +guards ArchSign/ArchFloatToBitPattern/... on +ARCH_CPU_INTEL || ARCH_CPU_ARM || ARCH_OS_WASM_VM and ends in #error Unknown +system architecture (the code it guards is portable IEEE-754 bit twiddling, +not x86 or ARM specific), and nonLockingLinux__execve() in +arch/stackTrace.cpp has hand-written syscall asm for aarch64 and x86_64 with +#error Unknown architecture in the #else. + +Add an ARCH_CPU_RISCV define for __riscv, admit __riscv_xlen == 64 to +ARCH_BITS_64, let math.h's gate accept it, and give +nonLockingLinux__execve() a riscv64 branch. The syscall branch mirrors the +aarch64 one exactly: the Linux riscv64 ABI passes arguments in a0-a2, takes +the syscall number in a7, uses the same generic __NR_execve == 221 as +aarch64, and traps with ecall instead of svc. Everything else in arch is +already portable: ARCH_SPIN_PAUSE() has a no-op #else, timing.h only reaches +rdtsc behind PXR_ARCH_PREFER_TSC_TIMING and otherwise uses +std::chrono::steady_clock, and the remaining #error branches in +systemInfo.cpp/fileSystem.cpp/assumptions.cpp select on the OS, not the CPU. + +With this, the core (non-imaging) USD libraries and their Python bindings +build and run on riscv64; pxr/base/gf's SIMD paths are already gated on +__SSE2__/__ARM_NEON with scalar fallbacks, and the vendored +double-conversion already lists __riscv. + +Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against PixarAnimationStudios/OpenUSD] +--- + pxr/base/arch/defines.h | 7 +++++-- + pxr/base/arch/math.h | 1 + + pxr/base/arch/overview.dox | 1 + + pxr/base/arch/stackTrace.cpp | 14 ++++++++++++++ + 4 files changed, 21 insertions(+), 2 deletions(-) + +diff --git a/pxr/base/arch/defines.h b/pxr/base/arch/defines.h +index b7ed634..58e860c 100644 +--- a/pxr/base/arch/defines.h ++++ b/pxr/base/arch/defines.h +@@ -39,6 +39,8 @@ + #elif defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || \ + defined(_M_ARM64) + #define ARCH_CPU_ARM ++#elif defined(__riscv) ++#define ARCH_CPU_RISCV + #endif + + // +@@ -46,12 +48,13 @@ + // + + #if defined(__x86_64__) || defined(__aarch64__) || defined(_M_X64) || \ +- defined(_M_ARM64) || defined(__wasm64__) ++ defined(_M_ARM64) || defined(__wasm64__) || \ ++ (defined(__riscv) && __riscv_xlen == 64) + #define ARCH_BITS_64 + #elif defined(__wasm32__) + #define ARCH_BITS_32 + #else +-#error "Unsupported architecture. x86_64 or ARM64 required." ++#error "Unsupported architecture. x86_64, ARM64 or RISC-V 64 required." + #endif + + // +diff --git a/pxr/base/arch/math.h b/pxr/base/arch/math.h +index ccd53a6..8191023 100644 +--- a/pxr/base/arch/math.h ++++ b/pxr/base/arch/math.h +@@ -30,6 +30,7 @@ PXR_NAMESPACE_OPEN_SCOPE + ///@{ + + #if defined (ARCH_CPU_INTEL) || defined (ARCH_CPU_ARM) || \ ++ defined (ARCH_CPU_RISCV) || \ + defined(ARCH_OS_WASM_VM) || defined (doxygen) + + /// This is the smallest value e such that 1+e^2 == 1, using floats. +diff --git a/pxr/base/arch/overview.dox b/pxr/base/arch/overview.dox +index b8eaf9b..045375b 100644 +--- a/pxr/base/arch/overview.dox ++++ b/pxr/base/arch/overview.dox +@@ -64,6 +64,7 @@ For processor: + + + For bits: +diff --git a/pxr/base/arch/stackTrace.cpp b/pxr/base/arch/stackTrace.cpp +index 384959e..6a90d38 100644 +--- a/pxr/base/arch/stackTrace.cpp ++++ b/pxr/base/arch/stackTrace.cpp +@@ -878,6 +878,20 @@ nonLockingLinux__execve (const char *file, + ); + result = __file_result; + } ++#elif defined (ARCH_CPU_RISCV) && defined(ARCH_BITS_64) ++ { ++ long __file_result asm ("a0") = (long)file; ++ char* const* __argv asm ("a1") = argv; ++ char* const* __envp asm ("a2") = envp; ++ long __num_execve asm ("a7") = 221; ++ __asm__ __volatile__ ( ++ "ecall" ++ : "=r" (__file_result) ++ : "r"(__num_execve), "r" (__file_result), "r" (__argv), "r" (__envp) ++ : "memory" ++ ); ++ result = __file_result; ++ } + #elif defined(ARCH_CPU_INTEL) && defined(ARCH_BITS_64) + + /* +-- +2.43.0 + diff --git a/patches/usd-exchange/3.0.1/0002-Skip-the-cache-line-size-assumption-check-on-riscv64.patch b/patches/usd-exchange/3.0.1/0002-Skip-the-cache-line-size-assumption-check-on-riscv64.patch new file mode 100644 index 000000000000..23a83b30b70a --- /dev/null +++ b/patches/usd-exchange/3.0.1/0002-Skip-the-cache-line-size-assumption-check-on-riscv64.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 4 Oct 2026 01:30:19 +0000 +Subject: [PATCH] Skip the cache-line size assumption check on riscv64 + +Arch_ValidateAssumptions() runs on every load of libusd_arch and compares +ARCH_CACHE_LINE_SIZE (pxr/base/arch/align.h: 128 on Apple ARM, 64 everywhere +else) with Arch_ObtainCacheLineSize(), i.e. sysconf(_SC_LEVEL1_DCACHE_LINESIZE) +on Linux. On a mismatch it prints a four-line " ArchWarn: ARCH_CACHE_LINE_SIZE +!= Arch_ObtainCacheLineSize()" block straight to stderr with fprintf +(arch/error.cpp), below the Tf diagnostics layer, so no environment setting or +diagnostic delegate can silence it. + +On riscv64 the comparison has nothing to check. The ISA defines no cache-line +size; it varies between implementations. glibc's riscv sysconf reads it from +the AT_L1D_CACHEGEOMETRY auxv entry, which the kernel fills from the +firmware's devicetree/ACPI cache description and sets to 0 when there is none +(arch/riscv/kernel/cacheinfo.c). Whatever value align.h picks for riscv64 is +therefore "wrong" on some machines, and the warning fires on every process +that imports pxr on those machines: every user of the wheel sees it, and +every test that checks a subprocess's stderr exactly fails because of it +(usd-exchange's testDiagnostics.py assertOutputStreams() and +testSettings.py's empty-stderr assertion). + +Keep ARCH_CACHE_LINE_SIZE at its 64 default (it only sizes padding and +alignment, so a mismatch costs performance, never correctness) and skip the +comparison when ARCH_CPU_RISCV is defined (added by the previous patch). The +other checks in Arch_ValidateAssumptions(), including the big-endian +ARCH_ERROR, are unchanged. + +Upstream-Status: To upstream [not submitted from this automated port run; belongs with the riscv64 arch support of the previous patch in a pull request against PixarAnimationStudios/OpenUSD] +--- + pxr/base/arch/assumptions.cpp | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/pxr/base/arch/assumptions.cpp b/pxr/base/arch/assumptions.cpp +index 1029b32..f0c63df 100644 +--- a/pxr/base/arch/assumptions.cpp ++++ b/pxr/base/arch/assumptions.cpp +@@ -135,10 +135,18 @@ Arch_ValidateAssumptions() + /* + * Make sure that the ARCH_CACHE_LINE_SIZE constant is set as expected + * on the current hardware architecture. ++ * ++ * RISC-V has no architectural cache-line size: it varies between ++ * implementations, and Linux reports whatever the firmware describes ++ * (0 when it describes none), so there is no constant to validate. + */ ++#if !defined(ARCH_CPU_RISCV) + if (ARCH_CACHE_LINE_SIZE != cacheLineSize) { + ARCH_WARNING("ARCH_CACHE_LINE_SIZE != Arch_ObtainCacheLineSize()"); + } ++#else ++ (void)cacheLineSize; ++#endif + + /* + * Make sure that the machine is little-endian. We do not support +-- +2.43.0 +