Skip to content

ci: pin exact Xcode 26.3 on macOS jobs (definitive libz.tbd fix) - #758

Merged
fernandotonon merged 1 commit into
masterfrom
fix/macos-pin-exact-xcode
Jun 23, 2026
Merged

ci: pin exact Xcode 26.3 on macOS jobs (definitive libz.tbd fix)#758
fernandotonon merged 1 commit into
masterfrom
fix/macos-pin-exact-xcode

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Definitive fix for the recurring macOS libz.tbd deploy failure.

Root cause (peeled back over several attempts): the macOS jobs run on runner images whose newest Xcode differs (one job resolved 26.5, another 26.3). The OGRE/Assimp SDKs bake the active SDK's absolute libz.tbd path into their CMake export, so a producer that built under 26.5 cached an SDK the 26.3 consumer can't link (No rule to make target '.../Xcode_26.5/...libz.tbd'). sort -V | tail -1 (newest) is non-deterministic across images; SDKROOT pinning helped the app's own ZLIB but the cached Codec_Assimp/ogre dylibs still carried the producer's path.

Fix: pin a SPECIFIC Xcode (26.3, present on all current macos-latest images) on all three macOS jobs, falling back to newest only if absent — so producers and consumer always agree on the SDK. Bump MACOS_CACHE_VERSIONxc263pin to discard the assimp+ogre caches that still carry a 26.5 path. (Keeps the SDKROOT pin + self-heal OGRE rebuild from the prior fixes as belt-and-suspenders.)

Needed to complete the 3.9.1 macOS deploy (Windows + Linux already published; macOS artifact + Homebrew cask pending).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated macOS CI/CD pipeline cache versioning to force rebuild when necessary.
    • Modified Xcode SDK selection logic to use pinned installation with fallback to automatic detection for improved build consistency.

Even with SDKROOT pinned, build-macos failed because the macOS jobs ran on
runner images with different *newest* Xcodes: the ogre/assimp producers
sometimes resolved Xcode 26.5 (newest on their image) and cached an SDK whose
Codec_Assimp/ogre CMake export hardcodes 26.5's libz.tbd path, while the
consumer (26.3) couldn't link it. "sort -V | tail -1" is non-deterministic
across images.

Pin a SPECIFIC Xcode (26.3) that's present on all current macos-latest images,
falling back to newest only if absent — so producers and consumer always agree
on the SDK. Bump MACOS_CACHE_VERSION → xc263pin to discard the assimp+ogre
caches that still carry a 26.5 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR bumps the MACOS_CACHE_VERSION env var from sdkpin1 to xc263pin to force cache invalidation, and replaces the dynamic "newest Xcode" selection logic in all three macOS build jobs (build-n-cache-assimp-macos, build-n-cache-ogre-macos, build-macos) with logic that pins to /Applications/Xcode_26.3.app/Contents/Developer when present, falling back to the previous newest-available detection otherwise.

Changes

Xcode 26.3 pin and cache invalidation

Layer / File(s) Summary
Cache version bump
.github/workflows/deploy.yml
MACOS_CACHE_VERSION changed from sdkpin1 to xc263pin to invalidate all macOS job caches.
Pinned Xcode selection across macOS build jobs
.github/workflows/deploy.yml
DEV selection in build-n-cache-assimp-macos, build-n-cache-ogre-macos, and build-macos updated to prefer the fixed Xcode 26.3 developer directory, with fallback to newest-Xcode detection when absent.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • fernandotonon/QtMeshEditor#756: Modifies the same deploy.yml file to pin the macOS Xcode toolchain and bump cache version tags for the OGRE/macOS build jobs — directly the same file and same pattern as this PR.

Poem

🐰 Hoppity-hop through the CI lane,
Xcode 26.3 is the one we'll retain!
sdkpin1 is gone, xc263pin is here,
With a fallback in place so builds stay clear.
No more stale caches to muddy the trail —
The bunny ensures every macOS job shall prevail! 🍎

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description explains the root cause, solution, and impact clearly, but does not follow the required template structure with Summary, Technical Details, and other sections. Restructure the description to match the template: add a Summary section, list Technical Details as bullet points, and include relevant checklist items if applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: pinning Xcode 26.3 on macOS CI jobs to fix the libz.tbd deployment issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/macos-pin-exact-xcode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/deploy.yml (1)

1577-1590: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the pinned Xcode path across the three macOS jobs.

Line 1585, Line 1664, and Line 1752 repeat the same hardcoded pin. Centralizing it in workflow env reduces drift risk when the pin changes again.

♻️ Suggested refactor
 env:
   BUILD_TYPE: Release
   AQT_VERSION: '==3.3.*'
   QT_VERSION: '6.9.3'
+  PINNED_XCODE_DEV_DIR: '/Applications/Xcode_26.3.app/Contents/Developer'
   ASSIMP_VERSION: '6.0.4'
-            PIN="/Applications/Xcode_26.3.app/Contents/Developer"
+            PIN="$PINNED_XCODE_DEV_DIR"
             if [ -d "$PIN" ]; then
                 DEV="$PIN"
             else
                 DEV=$(ls -d /Applications/Xcode_*.app/Contents/Developer 2>/dev/null | sort -V | tail -1)
             fi

Also applies to: 1662-1669, 1750-1757

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 1577 - 1590, The hardcoded Xcode
pin "Xcode_26.3.app" is repeated in three separate macOS job sections at lines
1585, 1664, and 1752 in the workflow file. To reduce drift risk when this pin
needs to be updated in the future, extract this value into a single environment
variable defined at the top-level workflow `env` section, and then reference
that variable (using ${{ env.VARIABLE_NAME }}) in all three job locations where
the PIN variable is currently set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/deploy.yml:
- Around line 1577-1590: The hardcoded Xcode pin "Xcode_26.3.app" is repeated in
three separate macOS job sections at lines 1585, 1664, and 1752 in the workflow
file. To reduce drift risk when this pin needs to be updated in the future,
extract this value into a single environment variable defined at the top-level
workflow `env` section, and then reference that variable (using ${{
env.VARIABLE_NAME }}) in all three job locations where the PIN variable is
currently set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46921318-a1df-49e5-9fac-a079e6f6037a

📥 Commits

Reviewing files that changed from the base of the PR and between dc03896 and 97b0a0b.

📒 Files selected for processing (1)
  • .github/workflows/deploy.yml

@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit 486704c into master Jun 23, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the fix/macos-pin-exact-xcode branch June 23, 2026 23:41
fernandotonon added a commit that referenced this pull request Jun 24, 2026
…embedding) (#754)

* feat(#407): native auto-rig core + CLI rig subcommand

Pinocchio (Baran & Popović 2007) is LGPL-2.1, which conflicts with the
project's statically-linked permissive-distribution stance — so, like #401
(Instant Meshes) and #402 (libigl/TetGen), this is a native from-scratch
implementation of the published *algorithm* (skeleton-template embedding),
zero new deps.

- AutoRig (src/AutoRig.h/.cpp): Ogre-free pure-data core — built-in templates
  (humanoid 19-bone / biped / quadruped / generic), fitTemplate() maps a
  template's normalised joint graph into the mesh AABB then recentres flagged
  joints toward per-height-slab centroids (spine→medial line, limb roots inside
  the silhouette). rigEntity() builds an Ogre::Skeleton (parent-relative bone
  positions, setBindingPose), binds via mesh->_notifySkeleton + entity
  ->_initialise(true) — the _initialise is REQUIRED or the exporters
  (both gate on entity->hasSkeleton()) silently drop the new rig.
- AutoRigController (QML singleton, mirrors SkinWeightsController) for the GUI.
- CLI: `qtmesh rig <file> [--skeleton T] [--skin] [--up-axis x|y|z] -o out`
  (cmdRig) — import, rig, optionally chain SkinWeights::computeAndApply, export.
  Registered in run() dispatch + AppLaunchHandler subcommand list.
- AutoRig_test.cpp: pure-data unit tests (template well-formedness, AABB
  containment, vertical ordering, degenerate-input robustness, string/JSON).
- Sentry breadcrumb ai.assist.auto_rig.

Verified end-to-end: static OBJ -> 19-bone humanoid + skin -> glTF export with
1 skin / 17 joints; FBX export carries the skeleton too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#407): MCP auto_rig tool + GUI Auto-Rig + rig CLI tests + docs

- MCP: `auto_rig` { template, skin?, up_axis?, output_path? }
  (MCPServer::toolAutoRig) — rigs the selected static mesh, optional skin chain
  + optional re-export. Registered + advertised. Breadcrumb ai.assist.auto_rig.
- GUI: AutoRigDialog.qml (template + up-axis pickers, "also skin" checkbox)
  driven by AutoRigController; new "Rigging" CollapsibleSection in Animation
  Mode → Mode Tools, gated on AutoRigController.hasRiggableSelection (a static
  mesh — already-rigged meshes show "Skinning" instead). Lazy-loaded Loader +
  openAutoRigDialog(), registered in qml_resources.qrc.
- Tests: CLIPipeline_cmdrig_coverage_test.cpp (arg-validation + file-missing
  branches need no GL; success path skips gracefully without Xvfb).
- CLAUDE.md: CLI examples (skin + rig), recognized-subcommand list, and a full
  AutoRig architecture entry (incl. the LGPL→native rationale, the
  _initialise(true) export gotcha, and documented quality limits).

App + UnitTests build clean on macOS arm64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): compile AutoRig.cpp into the test common lib

unit-tests-linux failed to link: AutoRig::* symbols undefined in
libqtmesh_test_common.a (MCPServer::toolAutoRig and CLIPipeline::cmdRig
reference them). The test target has its own TEST_SRC_FILES list separate from
the app's src/CMakeLists.txt — add AutoRig.cpp + AutoRigController.cpp there,
next to SkinWeights (same omission class as #738's PbrMapSynth gap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: re-trigger CI for #407 (missed synchronize webhook)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(gui): blank QML panels + white models in installed builds (#755)

* fix(gui): blank QML panels + white models in installed builds

Two packaging/runtime bugs that only manifested in installed builds (Homebrew
.app, .deb) — dev SDK runs masked both:

1. Blank white QML panels (Inspector / Context / Material docks). The Qt Quick
   *software* scene-graph backend was forced in the MainWindow ctor — AFTER
   QApplication, by which point Qt has already locked the default RHI. Moved
   QSG_RHI_BACKEND / setGraphicsApi(Software) to the top of main(), before
   QApplication (the only point where it takes effect). Replaced the now-dead
   call in mainwindow.cpp with a note.

2. White / untextured models in the macOS .app. Relative resource locations
   from resources.cfg were resolved against macBundlePath() (the .app bundle
   ROOT), but the media tree lives under Contents/MacOS/media (==
   applicationDirPath()). So <App>.app/media/... didn't exist, Ogre loaded no
   RTSS GLSL programs or textures, and every mesh rendered flat white. Resolve
   relative paths against applicationDirPath() first (matches Linux), with the
   bundle root kept as a fallback; only add a location if it exists. Verified:
   a macdeployqt'd bundle now loads media from Contents/MacOS/media and renders
   the mage.glb fully textured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version to 3.9.1 (installed-build QML + macOS white-model fix)

Bugfix release: QML software-backend set before QApplication, and macOS .app
resource paths resolved against applicationDirPath() instead of the bundle root.
Synced README + qtmesh action ref (verify-doc-versions gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): address review — QML registration, upAxis, error paths

Code review (Codex + CodeRabbit) on the auto-rig PR:

- CRITICAL: register AutoRigController as a QML singleton in mainwindow.cpp
  (PropertiesPanel URI) like the sibling controllers + add its kill(). With
  qt_add_qml_module disabled, QML_SINGLETON alone doesn't expose it, so the
  Rigging section/dialog would ReferenceError. Verified no error at runtime now.
- CRITICAL: the dialog's Up-axis picker was ignored — autoRigSelected() didn't
  take upAxis. Added a `const QString& upAxis` param (controller maps x/y/z →
  Options::upAxis) and pass dialog.upAxes[dialog.upAxisIndex] from QML.
- AutoRig::appendPositions: guard a null vbuf->lock() (shrink `out` back, return
  false) instead of dereferencing.
- AutoRig::rigEntity: on _initialise failure, detach the half-built skeleton
  (mesh->_notifySkeleton(null)) before removing it, so hasSkeleton() resets and
  a retry / exporter doesn't pick up a partial rig.
- MCP toolAutoRig: validate output_path type; a requested skin that fails is now
  a hard error (no unskinned export reported as success); export wrapped in the
  try/catch (also catches std::exception); Sentry breadcrumb no longer logs the
  full output path.
- PropertiesPanel openAutoRigDialog(): handle Loader.Error to allow retry.

App + UnitTests build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: key macOS caches on the resolved Xcode version (fix libz.tbd mismatch)

build-macos kept failing with "No rule to make target
'.../Xcode_26.5.../libz.tbd'" even after the Pin-Xcode step: the producer
(build-n-cache-ogre-macos) resolved "newest" to Xcode 26.3 on its runner image
and cached OGRE with 26.3's absolute libz.tbd path, while the consumer
(build-macos) resolved 26.5 and linked against the missing path. "newest"
(sort -V | tail -1) is NOT deterministic across the per-job runner images.

Fold the resolved Xcode app name into XCODE_TAG (exported by the Pin step) and
append it to all macOS assimp/ogre cache keys + restore-keys. A consumer on a
different Xcode now cache-misses and rebuilds OGRE/Assimp against its own SDK
instead of linking a stale path. (Belongs on master too — same deploy.yml.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: bust stale macOS OGRE cache (xcode263) — fixes libz.tbd link error

build-macos (incl. the 3.9.1 release deploy) failed:
  No rule to make target '.../Xcode_26.5/.../libz.tbd', needed by QtMeshEditor

Diagnosis: the Pin-Xcode step reliably selects Xcode 26.3 on ALL macOS jobs
(verified across runs), so compilation is consistent — but the restored OGRE
cache was built earlier under Xcode 26.5 and its CMake export hardcodes 26.5's
libz.tbd path. Restoring that into a 26.3 build breaks the link.

Fix: bump MACOS_CACHE_VERSION xcode26b → xcode263 so OGRE/Assimp are rebuilt
under the currently-pinned Xcode (26.3) and the stale 26.5 cache is discarded.

Also reverted the earlier XCODE_TAG-in-cache-key experiment: build-macos only
RESTORES the OGRE cache (no rebuild step), so a per-job Xcode-keyed miss would
leave it with no OGRE at all ("Could not find OGRE"). With Xcode pinned
consistently, a plain version bump is the correct, sufficient fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: self-heal macOS OGRE cache across differing per-job Xcode images

The real cause of the build-macos libz.tbd failure: the producer
(build-n-cache-ogre-macos) and consumer (build-macos) run on DIFFERENT runner
images whose "newest Xcode" differs — producer resolved Xcode 26.5 and cached
OGRE with 26.5's absolute libz.tbd path baked into its CMake export; consumer
resolved 26.3 and linked against the missing 26.5 path. Just pinning "newest"
or bumping the cache version doesn't help because the two images disagree.

Fix (self-healing):
- Fold the resolved Xcode app name into XCODE_TAG and append it to all macOS
  assimp/ogre cache keys + restore-keys, so a job only restores a cache built
  under its OWN Xcode.
- Give build-macos (consumer) the same "check out + build OGRE on cache miss"
  steps the producer has. When the consumer's Xcode differs from the producer's
  (cache miss), it rebuilds OGRE under its own SDK instead of failing on a stale
  libz.tbd path.

This makes the macOS build robust regardless of which Xcode each runner image
ships. (Bigger than the earlier one-line bump, but that couldn't fix a
cross-image Xcode disagreement.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: keep assimp macOS cache Xcode-agnostic (only ogre is Xcode-keyed)

Previous commit Xcode-keyed BOTH the assimp and ogre macOS caches. That broke
build-macos on a runner whose Xcode differed from the producer's: assimp
cache-missed (no assimp-build-on-miss exists) so find_package(assimp) failed
with "Could not find a package configuration file provided by assimp".

Assimp is a plain static lib that doesn't bake absolute SDK paths, so one
assimp cache is valid across Xcode versions — revert XCODE_TAG on the 3 assimp
keys, keeping it ONLY on the 2 ogre keys (ogre's CMake export DOES bake an
absolute libz.tbd path, which is why ogre needs per-Xcode keying + the
consumer's rebuild-on-miss). The shared assimp cache is then always present for
the ogre rebuild to link against.

Verified on the failing run: Qt + OGRE now resolve and link (no libz.tbd
error); this removes the remaining assimp-not-found failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: fix flaky MainWindowTest.ModeBarLoadsAndModeChange (show window first)

This test failed intermittently on CI (Xvfb) with:
  Value of: window->m_modeBarShell->isHidden()  Actual: true  Expected: false

The fixture constructs MainWindow but never show()s it. QToolBar::isHidden()
reflects effective visibility, which is only realized once the parent window is
mapped — so under Xvfb the shell reports hidden and the assertion is racy. It hit
BOTH this branch and the unrelated CI-only PR #756 (which has no source changes),
confirming it's a pre-existing flake, not a regression.

Fix: show() the window and processEvents() before the visibility assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: pin SDKROOT so CMake ZLIB resolves under the selected Xcode (macOS)

build-macos still failed with the Xcode_26.5 libz.tbd path even after pinning
DEVELOPER_DIR=Xcode_26.3 and rebuilding OGRE: CMake's find_package(ZLIB)
resolved to the SDK that `xcrun` defaults to (26.5 on these images) rather than
the xcode-select'd one, so the OGRE SDK's CMake export baked a 26.5 libz.tbd
path that the cache then carried forward.

Fix: export SDKROOT (from `xcrun --sdk macosx --show-sdk-path` under the pinned
Xcode) in the Pin step, so clang AND CMake resolve system libs under the SAME
pinned SDK on every macOS job. Bump MACOS_CACHE_VERSION → sdkpin1 to discard the
OGRE caches that still carry the 26.5 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: bust stale macOS OGRE cache (xcode263) — unblock 3.9.1 macOS deploy (#756)

* ci: bust stale macOS OGRE cache (xcode263) — unblock the macOS deploy

The 3.9.1 release deploy failed on build-macos:
  No rule to make target '.../Xcode_26.5/.../libz.tbd', needed by QtMeshEditor

The Pin-Xcode step selects Xcode 26.3 consistently on all macOS jobs, but the
OGRE cache under key 'xcode26b' was built earlier under Xcode 26.5 and its
CMake export hardcodes 26.5's libz.tbd path. Restoring it into a 26.3 build
breaks the link. Bump MACOS_CACHE_VERSION xcode26b → xcode263 so OGRE/Assimp
rebuild under the pinned 26.3 and the stale cache is discarded.

(Windows + Linux .deb artifacts already published for 3.9.1; this lets the
macOS artifact + Homebrew cask update complete on a deploy re-run.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: self-heal macOS OGRE cache across differing per-job Xcode images

The real cause of the build-macos libz.tbd failure: the producer
(build-n-cache-ogre-macos) and consumer (build-macos) run on DIFFERENT runner
images whose "newest Xcode" differs — producer resolved Xcode 26.5 and cached
OGRE with 26.5's absolute libz.tbd path baked into its CMake export; consumer
resolved 26.3 and linked against the missing 26.5 path. Just pinning "newest"
or bumping the cache version doesn't help because the two images disagree.

Fix (self-healing):
- Fold the resolved Xcode app name into XCODE_TAG and append it to all macOS
  assimp/ogre cache keys + restore-keys, so a job only restores a cache built
  under its OWN Xcode.
- Give build-macos (consumer) the same "check out + build OGRE on cache miss"
  steps the producer has. When the consumer's Xcode differs from the producer's
  (cache miss), it rebuilds OGRE under its own SDK instead of failing on a stale
  libz.tbd path.

This makes the macOS build robust regardless of which Xcode each runner image
ships. (Bigger than the earlier one-line bump, but that couldn't fix a
cross-image Xcode disagreement.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: keep assimp macOS cache Xcode-agnostic (only ogre is Xcode-keyed)

Previous commit Xcode-keyed BOTH the assimp and ogre macOS caches. That broke
build-macos on a runner whose Xcode differed from the producer's: assimp
cache-missed (no assimp-build-on-miss exists) so find_package(assimp) failed
with "Could not find a package configuration file provided by assimp".

Assimp is a plain static lib that doesn't bake absolute SDK paths, so one
assimp cache is valid across Xcode versions — revert XCODE_TAG on the 3 assimp
keys, keeping it ONLY on the 2 ogre keys (ogre's CMake export DOES bake an
absolute libz.tbd path, which is why ogre needs per-Xcode keying + the
consumer's rebuild-on-miss). The shared assimp cache is then always present for
the ogre rebuild to link against.

Verified on the failing run: Qt + OGRE now resolve and link (no libz.tbd
error); this removes the remaining assimp-not-found failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: fix flaky MainWindowTest.ModeBarLoadsAndModeChange (show window first)

This test failed intermittently on CI (Xvfb) with:
  Value of: window->m_modeBarShell->isHidden()  Actual: true  Expected: false

The fixture constructs MainWindow but never show()s it. QToolBar::isHidden()
reflects effective visibility, which is only realized once the parent window is
mapped — so under Xvfb the shell reports hidden and the assertion is racy. It hit
BOTH this branch and the unrelated CI-only PR #756 (which has no source changes),
confirming it's a pre-existing flake, not a regression.

Fix: show() the window and processEvents() before the visibility assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: pin SDKROOT so CMake ZLIB resolves under the selected Xcode (macOS)

build-macos still failed with the Xcode_26.5 libz.tbd path even after pinning
DEVELOPER_DIR=Xcode_26.3 and rebuilding OGRE: CMake's find_package(ZLIB)
resolved to the SDK that `xcrun` defaults to (26.5 on these images) rather than
the xcode-select'd one, so the OGRE SDK's CMake export baked a 26.5 libz.tbd
path that the cache then carried forward.

Fix: export SDKROOT (from `xcrun --sdk macosx --show-sdk-path` under the pinned
Xcode) in the Pin step, so clang AND CMake resolve system libs under the SAME
pinned SDK on every macOS job. Bump MACOS_CACHE_VERSION → sdkpin1 to discard the
OGRE caches that still carry the 26.5 path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: pin EXACT Xcode 26.3 on all macOS jobs (stop per-image newest drift) (#758)

Even with SDKROOT pinned, build-macos failed because the macOS jobs ran on
runner images with different *newest* Xcodes: the ogre/assimp producers
sometimes resolved Xcode 26.5 (newest on their image) and cached an SDK whose
Codec_Assimp/ogre CMake export hardcodes 26.5's libz.tbd path, while the
consumer (26.3) couldn't link it. "sort -V | tail -1" is non-deterministic
across images.

Pin a SPECIFIC Xcode (26.3) that's present on all current macos-latest images,
falling back to newest only if absent — so producers and consumer always agree
on the SDK. Bump MACOS_CACHE_VERSION → xc263pin to discard the assimp+ogre
caches that still carry a 26.5 path.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): restore textured viewport and paint preview on File→Open (#757)

* fix(import): restore textured viewport and paint preview on File→Open

Rebind RTSS materials for newly imported entities (same path cloud downloads
already used) and load texture-paint buffers from embedded/disk sources before
GPU readback, which fails for many imported FBX textures on Linux.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(import): scope texture rebind per entity source file

Use each mesh's qtme.source_path binding for sidecar texture lookup so
multi-file File→Open imports cannot cross-bind common texture basenames.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: bump version to 3.9.2

Sync README and website pinned refs via sync-doc-versions-from-cmake.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(#407): native auto-rig core + CLI rig subcommand

Pinocchio (Baran & Popović 2007) is LGPL-2.1, which conflicts with the
project's statically-linked permissive-distribution stance — so, like #401
(Instant Meshes) and #402 (libigl/TetGen), this is a native from-scratch
implementation of the published *algorithm* (skeleton-template embedding),
zero new deps.

- AutoRig (src/AutoRig.h/.cpp): Ogre-free pure-data core — built-in templates
  (humanoid 19-bone / biped / quadruped / generic), fitTemplate() maps a
  template's normalised joint graph into the mesh AABB then recentres flagged
  joints toward per-height-slab centroids (spine→medial line, limb roots inside
  the silhouette). rigEntity() builds an Ogre::Skeleton (parent-relative bone
  positions, setBindingPose), binds via mesh->_notifySkeleton + entity
  ->_initialise(true) — the _initialise is REQUIRED or the exporters
  (both gate on entity->hasSkeleton()) silently drop the new rig.
- AutoRigController (QML singleton, mirrors SkinWeightsController) for the GUI.
- CLI: `qtmesh rig <file> [--skeleton T] [--skin] [--up-axis x|y|z] -o out`
  (cmdRig) — import, rig, optionally chain SkinWeights::computeAndApply, export.
  Registered in run() dispatch + AppLaunchHandler subcommand list.
- AutoRig_test.cpp: pure-data unit tests (template well-formedness, AABB
  containment, vertical ordering, degenerate-input robustness, string/JSON).
- Sentry breadcrumb ai.assist.auto_rig.

Verified end-to-end: static OBJ -> 19-bone humanoid + skin -> glTF export with
1 skin / 17 joints; FBX export carries the skeleton too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#407): MCP auto_rig tool + GUI Auto-Rig + rig CLI tests + docs

- MCP: `auto_rig` { template, skin?, up_axis?, output_path? }
  (MCPServer::toolAutoRig) — rigs the selected static mesh, optional skin chain
  + optional re-export. Registered + advertised. Breadcrumb ai.assist.auto_rig.
- GUI: AutoRigDialog.qml (template + up-axis pickers, "also skin" checkbox)
  driven by AutoRigController; new "Rigging" CollapsibleSection in Animation
  Mode → Mode Tools, gated on AutoRigController.hasRiggableSelection (a static
  mesh — already-rigged meshes show "Skinning" instead). Lazy-loaded Loader +
  openAutoRigDialog(), registered in qml_resources.qrc.
- Tests: CLIPipeline_cmdrig_coverage_test.cpp (arg-validation + file-missing
  branches need no GL; success path skips gracefully without Xvfb).
- CLAUDE.md: CLI examples (skin + rig), recognized-subcommand list, and a full
  AutoRig architecture entry (incl. the LGPL→native rationale, the
  _initialise(true) export gotcha, and documented quality limits).

App + UnitTests build clean on macOS arm64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): compile AutoRig.cpp into the test common lib

unit-tests-linux failed to link: AutoRig::* symbols undefined in
libqtmesh_test_common.a (MCPServer::toolAutoRig and CLIPipeline::cmdRig
reference them). The test target has its own TEST_SRC_FILES list separate from
the app's src/CMakeLists.txt — add AutoRig.cpp + AutoRigController.cpp there,
next to SkinWeights (same omission class as #738's PbrMapSynth gap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): address review — QML registration, upAxis, error paths

Code review (Codex + CodeRabbit) on the auto-rig PR:

- CRITICAL: register AutoRigController as a QML singleton in mainwindow.cpp
  (PropertiesPanel URI) like the sibling controllers + add its kill(). With
  qt_add_qml_module disabled, QML_SINGLETON alone doesn't expose it, so the
  Rigging section/dialog would ReferenceError. Verified no error at runtime now.
- CRITICAL: the dialog's Up-axis picker was ignored — autoRigSelected() didn't
  take upAxis. Added a `const QString& upAxis` param (controller maps x/y/z →
  Options::upAxis) and pass dialog.upAxes[dialog.upAxisIndex] from QML.
- AutoRig::appendPositions: guard a null vbuf->lock() (shrink `out` back, return
  false) instead of dereferencing.
- AutoRig::rigEntity: on _initialise failure, detach the half-built skeleton
  (mesh->_notifySkeleton(null)) before removing it, so hasSkeleton() resets and
  a retry / exporter doesn't pick up a partial rig.
- MCP toolAutoRig: validate output_path type; a requested skin that fails is now
  a hard error (no unskinned export reported as success); export wrapped in the
  try/catch (also catches std::exception); Sentry breadcrumb no longer logs the
  full output path.
- PropertiesPanel openAutoRigDialog(): handle Loader.Error to allow retry.

App + UnitTests build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#407): Mixamo-style marker placement + undoable rig/skin

Marker-guided auto-rig refinement: the user clicks 10 humanoid markers on
the mesh in the viewport (chin, L/R shoulder, L/R wrist, L/R hip, L/R knee,
pelvis) and each anchors its joint while the limb/spine chains interpolate
between the anchors — so the rig follows real (incl. cartoon) proportions
instead of the fixed proportional template.

- AutoRig::fitTemplateWithMarkers + layChain (generic anchor→tip chain):
  arms lay shoulder→arm→forearm→hand toward the wrist; legs lay hip
  socket→knee→foot; spine distributes Spine/Chest/Neck evenly between the
  marked pelvis and chin; hips carry unmarked thigh roots, explicit hip
  markers override. Every marker optional (empty set ≡ fitTemplate).
- AutoRigController marker session (begin/skip/undo/cancel/commit) with
  ray-picked PT_SPHERE overlays; clicks routed via TransformOperator before
  the knife/select paths. AutoRigDialog made non-modal so viewport clicks
  reach the scene; onClosing cancels any active session.
- Undo/redo: AutoRig::unrigEntity + AutoRigCommand wrap rig (+ optional skin)
  in one undoable unit; both GUI paths push through UndoManager. Single
  Ctrl+Z reverts rig and skin together.
- Skeleton section extracted in the Inspector (skeleton/weights toggles no
  longer gated on animations).
- Tests: marker order/labels, empty≡fitTemplate, arm/leg chain layout,
  shoulder/hip anchoring, spine interpolation, command error/undo branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#407): move auto-rig UI inline into Inspector + fix Skip

- Replace the modal AutoRigDialog with an inline Rigging section in the
  Inspector (riggingToolsComponent). Skeleton-type picker is a primary
  control; up-axis stays under "Advanced options". Smart show/hide: idle
  shows entry points + options, marker mode swaps to the guidance label +
  Skip/Undo/Cancel/Rig-from-markers controls. Section cancels any active
  marker session when it disappears (replaces the dialog's onClosing).
- Markers only offered for the humanoid template (they're humanoid-specific).
- Fix Skip: marker progress is now a CURSOR into the order list. Skip
  advances the cursor past a slot without storing a marker (joint keeps the
  template fit); the old code pushed an unset placeholder that didn't count
  as resolved, so the cursor stuck and Skip did nothing. Place advances +
  stores; Undo steps back, dropping the marker if that slot was placed.
  New markerPlacedCount drives "Rig from markers"; markerCount = resolved
  slots (placed + skipped) for the N/total readout.
- Remove qml/AutoRigDialog.qml + its qrc entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): undoable rig crash — strip blend vertex elements on unrig

Rigging with skin runs Ogre's _compileBoneAssignments, which adds
BLEND_INDICES/BLEND_WEIGHTS vertex elements. On undo, clearing the
bone-assignment list is NOT enough — Ogre only removes those elements
inside compileBoneAssignments, which it skips when the list is empty, so
the declaration kept advertising blend elements while the entity had no
skeleton → null SkeletonInstance deref on the next _initialise/render.

AutoRig::unrigEntity now explicitly strips the blend elements (unbind the
buffer + removeElement) on shared + per-submesh vertex data, leaving a
genuinely static mesh. AutoRigCommand also calls
AutoRigController::notifyRiggingChanged on redo/undo — on undo BEFORE
detaching, so any active skeleton-debug overlay tears down while the
skeleton still exists (else it dangles), and the Inspector re-evaluates
the Rigging/Skeleton sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(#407): coherent marker inference + mesh-bounds clamping

Replace per-marker patching with a resolve-then-lay model: fitTemplate
gives a proportional baseline + template segment vectors, then every key
joint (Head, L/R Shoulder, L/R Hand, Hips, L/R UpLeg, L/R Knee) is resolved
marked → inferred-from-marked-neighbours → template, and the dependent
chains (spine/arms/legs) are laid from those anchors. A partial marker set
now yields an anatomically-sane skeleton instead of stranding unmarked
joints at the template (no more shoulder-above-head).

Inference: Hips ← up-leg midpoint + template rise; Head ← template offset
above Hips; UpLeg ← mirror the other / pelvis + socket offset; Shoulder ←
along the live Hips→Head line at the template height fraction + lateral
offset (chin+hips imply sane shoulders), else mirror; Hand ← shoulder +
template arm vector (marked shoulder + skipped wrist still lays a full arm);
mirroring reflects across the auto-detected sagittal plane.

Mesh-bounds clamp: inferred legs no longer punch through the model. Knee
skipped → foot drops to the mesh floor (AABB mn[up]) below the up-leg, knee
halfway between; marked knee → foot extrapolated but floor-clamped; knee
always kept between up-leg and foot in the up axis.

Empty marker set still early-returns fitTemplate unchanged. Tests added for
each inference rule + the floor clamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#407): update stale 6-marker test to 10; bust stale macOS assimp cache

- AutoRigMarkers.OrderAndLabelsAreStable still asserted 6 markers (and
  order[5]==Hips) from the first marker commit; the set grew to 10 (added
  L/R shoulder + L/R hip). unit-tests-linux caught it. Assert 10 and use
  front()/back() so the count is the single source of truth.
- Bump MACOS_CACHE_VERSION sdkpin1→sdkpin2: the assimp macOS cache was built
  under Xcode 26.5 and baked .../MacOSX26.5.sdk/.../libz.tbd into Codec_Assimp,
  so OGRE's cache-miss rebuild under the pinned 26.3 failed with
  "No rule to make target .../libz.tbd". Busting the cache rebuilds assimp
  under the pinned SDK. (Pre-existing CI infra issue, not from this feature.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant