From 5e70371af8facdf21b196207fc93968a6ace0dc3 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:45 -0600 Subject: [PATCH 1/7] Install PyTorch through a temp dir on the same disk as the target pip unpacks the whole install (~7 GB with CUDA) into TMPDIR before moving it into --target, so the on-demand install failed with "No space left on device" on systems with a small /tmp. light-the-torch also exited 0 after pip failed, which produced a misleading "install completed but cannot be imported" error. Point TMPDIR at a temp dir next to the per-user site-packages, and verify that torch imports before reporting success, in both Studio/CLI and Python. --- Libs/Application/Job/PythonWorker.cpp | 26 +++++++++++++++++-- Libs/Application/Job/PythonWorker.h | 2 +- Python/shapeworks/shapeworks/torch_install.py | 21 +++++++++++---- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Libs/Application/Job/PythonWorker.cpp b/Libs/Application/Job/PythonWorker.cpp index 66bb6530b39..74f227d4665 100644 --- a/Libs/Application/Job/PythonWorker.cpp +++ b/Libs/Application/Job/PythonWorker.cpp @@ -15,6 +15,7 @@ using namespace pybind11::literals; // to bring in the `_a` literal #include #include #include +#include #include #include @@ -488,7 +489,7 @@ std::string PythonWorker::get_user_site_packages() { } //--------------------------------------------------------------------------- -bool PythonWorker::is_torch_available() { +bool PythonWorker::is_torch_available(int timeout_ms) { #ifdef SHAPEWORKS_BUNDLED_PYTHON std::string python_home = find_bundled_python_home(); if (python_home.empty()) { @@ -530,7 +531,7 @@ bool PythonWorker::is_torch_available() { #endif proc.setProcessEnvironment(env); proc.start(python_exe, {"-c", "import torch"}); - proc.waitForFinished(10000); + proc.waitForFinished(timeout_ms); return proc.exitCode() == 0; #else return true; // conda workflow — assume torch is available @@ -564,9 +565,23 @@ bool PythonWorker::install_torch(std::function output_callbac output_callback("This may take several minutes..."); } + // pip unpacks the whole install (~7 GB with CUDA) into TMPDIR before moving it + // into --target, which overflows a small /tmp. Use a temp dir next to the + // target instead, so it's on the same disk. + QTemporaryDir pip_tmp(QFileInfo(QString::fromStdString(user_sp)).absolutePath() + "/pip-tmp-XXXXXX"); + if (!pip_tmp.isValid()) { + if (output_callback) output_callback("Error: could not create temp directory: " + pip_tmp.errorString().toStdString()); + return false; + } + // Use light-the-torch to install the correct PyTorch variant QProcess proc; proc.setProcessChannelMode(QProcess::MergedChannels); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("TMPDIR", pip_tmp.path()); + env.insert("TEMP", pip_tmp.path()); + env.insert("TMP", pip_tmp.path()); + proc.setProcessEnvironment(env); // Connect output to callback if (output_callback) { @@ -597,6 +612,13 @@ bool PythonWorker::install_torch(std::function output_callbac return false; } + // light-the-torch can exit 0 even when pip fails (e.g. disk full), so check + // that torch actually imports. A first import of a fresh install can be slow. + if (!is_torch_available(120000)) { + if (output_callback) output_callback("Error: PyTorch installation failed; see the pip output above."); + return false; + } + if (output_callback) output_callback("PyTorch installed successfully."); return true; #else diff --git a/Libs/Application/Job/PythonWorker.h b/Libs/Application/Job/PythonWorker.h index 475e0dee900..c2c03e89b1d 100644 --- a/Libs/Application/Job/PythonWorker.h +++ b/Libs/Application/Job/PythonWorker.h @@ -42,7 +42,7 @@ class PythonWorker : public QObject { //! Check if PyTorch is available in the bundled Python. //! Returns true if torch can be imported successfully. - static bool is_torch_available(); + static bool is_torch_available(int timeout_ms = 10000); //! Install PyTorch via light-the-torch into the user site-packages directory. //! The output_callback receives lines of output from the install process. diff --git a/Python/shapeworks/shapeworks/torch_install.py b/Python/shapeworks/shapeworks/torch_install.py index 6081743121b..5e064c38a6a 100644 --- a/Python/shapeworks/shapeworks/torch_install.py +++ b/Python/shapeworks/shapeworks/torch_install.py @@ -10,8 +10,10 @@ import importlib import os import platform +import shutil import subprocess import sys +import tempfile # Must match PythonWorker::python_api_version in Libs/Application/Job/PythonWorker.h. # Bump alongside SW_MAJOR_VERSION / SW_MINOR_VERSION per docs/dev/release-checklist. @@ -62,10 +64,18 @@ def ensure_torch(): print(f"Installing PyTorch to {user_site}") print("This may take several minutes...") - subprocess.check_call([ - sys.executable, "-m", "light_the_torch", "install", - "--target", user_site, "torch", - ]) + # pip unpacks the whole install (~7 GB with CUDA) into TMPDIR before moving + # it into --target, which overflows a small /tmp. Use a temp dir next to + # the target instead, so it's on the same disk. + pip_tmp = tempfile.mkdtemp(prefix="pip-tmp-", dir=os.path.dirname(user_site)) + env = dict(os.environ, TMPDIR=pip_tmp, TEMP=pip_tmp, TMP=pip_tmp) + try: + subprocess.check_call([ + sys.executable, "-m", "light_the_torch", "install", + "--target", user_site, "torch", + ], env=env) + finally: + shutil.rmtree(pip_tmp, ignore_errors=True) importlib.invalidate_caches() if user_site not in sys.path: @@ -75,6 +85,7 @@ def ensure_torch(): import torch # noqa: F401 print("PyTorch installed successfully.") except ImportError as e: + # light-the-torch can exit 0 even when pip fails (e.g. disk full) raise RuntimeError( - f"PyTorch install completed but cannot be imported: {e}" + f"PyTorch installation failed; see the pip output above. ({e})" ) from e From 92d4ad968972dfb801eb9fc7404808fe39b4e300 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:45 -0600 Subject: [PATCH 2/7] Fix Windows uninstaller leaving registry keys and .swproj association The uninstaller unregistered ".swprojc" instead of ".swproj", and without SetRegView 64 it deleted its registry keys from the 32-bit view while the installer wrote them to the 64-bit view. --- Support/shapeworks.nsi | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Support/shapeworks.nsi b/Support/shapeworks.nsi index 2a07c9e41d7..bed2dfa1a44 100644 --- a/Support/shapeworks.nsi +++ b/Support/shapeworks.nsi @@ -121,7 +121,7 @@ Section "Uninstall" ; Remove shortcuts, if any Delete "$SMPROGRAMS\ShapeWorks\*.*" - ${unregisterExtension} ".swprojc" "ShapeWorks Project" + ${unregisterExtension} ".swproj" "ShapeWorks Project" ; Remove directories used RMDir "$SMPROGRAMS\ShapeWorks" @@ -137,3 +137,11 @@ Function .onInit SetRegView 64 ${EndIf} FunctionEnd + +; The installer writes its registry keys in the 64-bit view, so the +; uninstaller must use the same view to remove them. +Function un.onInit + ${If} ${RunningX64} + SetRegView 64 + ${EndIf} +FunctionEnd From fd74854b46753887e8be14fed7a1a7d445dbf904 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:45 -0600 Subject: [PATCH 3/7] Don't abort at startup on Linux when LANG names a missing locale initialize_path_handling() imbued a UTF-8 codecvt into boost::filesystem on every platform. On POSIX, imbue() first builds boost's default locale from the environment, which throws when LANG names a locale that isn't installed (e.g. LANG=en_US), aborting shapeworks, Studio and the Python module. The conversion only matters on Windows, so only do it there. --- Libs/Common/ShapeworksUtils.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libs/Common/ShapeworksUtils.cpp b/Libs/Common/ShapeworksUtils.cpp index 99799c8cfab..458db670131 100644 --- a/Libs/Common/ShapeworksUtils.cpp +++ b/Libs/Common/ShapeworksUtils.cpp @@ -34,8 +34,11 @@ void ShapeWorksUtils::initialize_path_handling() { // into boost::filesystem::path, which by default comes from the global locale rather than being // UTF-8. The rest of ShapeWorks (Qt, VTK, ITK) speaks UTF-8, so without this a path holding // non-ASCII characters is mangled on the way in and no longer resolves. On POSIX no conversion - // takes place and this only affects the wide-string accessors. + // takes place, so skip it there: imbue() first builds boost's default locale from the + // environment, which throws (and aborts at startup) when LANG names a locale that isn't installed. +#ifdef _WIN32 boost::filesystem::path::imbue(std::locale(std::locale(), new boost::nowide::utf8_codecvt)); +#endif } //----------------------------------------------------------------------------- From 94c489f3beb30a31946373e5b5fc2583da51129a Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:45 -0600 Subject: [PATCH 4/7] Point RunUseCase.py import errors at swpython instead of conda The message still told users to run install_shapeworks.sh and activate a conda env, which no longer applies with bundled Python. --- Examples/Python/RunUseCase.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Examples/Python/RunUseCase.py b/Examples/Python/RunUseCase.py index aa1a175f509..1393b426081 100644 --- a/Examples/Python/RunUseCase.py +++ b/Examples/Python/RunUseCase.py @@ -6,7 +6,7 @@ binaries. This step should be common for any use of a function in ShapeWorks. __This requires the full ShapeWorks to be correctly built/downloaded!__' -For users: Please make sure you have run "source install_shapeworks.sh" (once) and "conda activate shapeworks" (each time). +For users: Run this script with the swpython that ships with ShapeWorks, e.g. "swpython RunUseCase.py ellipsoid". For developers: Please make sure you have run "source ./devenv.sh " """ @@ -42,8 +42,7 @@ except ImportError as error: print("\nError: Unable to import ShapeWorks executables, Python module, or dependencies\n") - print( - "Users: Please make sure you have run \"source install_shapeworks.sh\" (once) and \"conda activate shapeworks\" (each time)") + print("Users: Please run this script with the swpython that ships with ShapeWorks, e.g. \"swpython RunUseCase.py ellipsoid\"") print("\tSee http://sciinstitute.github.io/ShapeWorks/users/install.html for more information") print("\nDevelopers: Please make sure you have run \"source ./devenv.sh \"") From d0700e01c3413ebe72ed541955e416a11b9f4624 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:46 -0600 Subject: [PATCH 5/7] Require macOS 14 and refuse older versions with a clear message 6.8 builds target macOS 14, but nothing stopped installing or running on older versions, where it failed with cryptic errors (e.g. numpy's Accelerate build missing symbols on macOS 12). The .pkg is now wrapped in a distribution package whose installation check refuses older macOS, Studio sets LSMinimumSystemVersion, and Studio, the shapeworks CLI and swpython check the OS version at startup. Document the requirement in the install docs and release notes. --- Applications/shapeworks/shapeworks.cpp | 7 ++++++ Libs/Common/ShapeworksUtils.cpp | 24 +++++++++++++++++++++ Libs/Common/ShapeworksUtils.h | 4 ++++ Studio/CMakeLists.txt | 6 ++++++ Studio/Resources/Info.plist.in | 2 ++ Studio/main.cpp | 7 ++++++ Support/osx_distribution.xml | 30 ++++++++++++++++++++++++++ Support/package.sh | 16 +++++++++++++- cmake/InstallBundledPython.cmake | 8 +++++++ docs/about/release-notes.md | 3 +++ docs/users/Mac_README.txt | 2 ++ docs/users/install.md | 5 +++++ 12 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 Support/osx_distribution.xml diff --git a/Applications/shapeworks/shapeworks.cpp b/Applications/shapeworks/shapeworks.cpp index ea12ab45297..7d03f9d4504 100644 --- a/Applications/shapeworks/shapeworks.cpp +++ b/Applications/shapeworks/shapeworks.cpp @@ -3,6 +3,7 @@ */ #include +#include #include "Executable.h" #include "Commands.h" #include @@ -12,6 +13,12 @@ using namespace shapeworks; int main(int argc, char *argv[]) { + std::string os_error = ShapeWorksUtils::check_minimum_os_version(); + if (!os_error.empty()) { + std::cerr << os_error << "\n"; + return 1; + } + ShapeWorksUtils::initialize_path_handling(); Executable shapeworks; diff --git a/Libs/Common/ShapeworksUtils.cpp b/Libs/Common/ShapeworksUtils.cpp index 458db670131..a5a677e300e 100644 --- a/Libs/Common/ShapeworksUtils.cpp +++ b/Libs/Common/ShapeworksUtils.cpp @@ -15,6 +15,11 @@ #include #include +#ifdef __APPLE__ +#include +#include +#endif + namespace shapeworks { unsigned ShapeWorksUtils::rng_seed_ = std::chrono::system_clock::now().time_since_epoch().count(); @@ -27,6 +32,25 @@ void ShapeWorksUtils::set_rng_seed(const unsigned seed) { mt_.seed(rng_seed_); } +//----------------------------------------------------------------------------- +std::string ShapeWorksUtils::check_minimum_os_version() { +#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MIN_REQUIRED) + // e.g. 140000 for a build targeting macOS 14.0 + const int required_major = __MAC_OS_X_VERSION_MIN_REQUIRED / 10000; + char version[32] = {}; + size_t size = sizeof(version); + if (sysctlbyname("kern.osproductversion", version, &size, nullptr, 0) != 0) { + return ""; // can't tell, so don't block + } + int major = std::atoi(version); + if (major > 0 && major < required_major) { + return "ShapeWorks requires macOS " + std::to_string(required_major) + " or later. This Mac is running macOS " + + std::string(version) + "."; + } +#endif + return ""; +} + //----------------------------------------------------------------------------- void ShapeWorksUtils::initialize_path_handling() { // boost::filesystem stores a path in the character type the OS API uses: char on POSIX, but diff --git a/Libs/Common/ShapeworksUtils.h b/Libs/Common/ShapeworksUtils.h index d4f5cf3a72f..330b18a10c3 100644 --- a/Libs/Common/ShapeworksUtils.h +++ b/Libs/Common/ShapeworksUtils.h @@ -31,6 +31,10 @@ class ShapeWorksUtils { /// generates random number static unsigned get_random_number() { return mt_(); } + //! Returns an error message if this OS is older than ShapeWorks supports, or "" if it's fine. + //! On macOS the floor is the deployment target the build was compiled for. + static std::string check_minimum_os_version(); + //! Configure process-wide filesystem path handling. Call this once at startup, before any path //! is constructed from a narrow string. static void initialize_path_handling(); diff --git a/Studio/CMakeLists.txt b/Studio/CMakeLists.txt index 6a5e028210c..7072a1ba56f 100644 --- a/Studio/CMakeLists.txt +++ b/Studio/CMakeLists.txt @@ -288,6 +288,12 @@ IF(APPLE) # set the version ($SHAPEWORKS_VERSION) SET(MACOSX_BUNDLE_SHORT_VERSION_STRING ${SHAPEWORKS_VERSION}) SET(MACOSX_BUNDLE_VERSION_STRING_STRING ${SHAPEWORKS_VERSION}) + # oldest macOS the app will launch on (LSMinimumSystemVersion) + IF(CMAKE_OSX_DEPLOYMENT_TARGET) + SET(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION ${CMAKE_OSX_DEPLOYMENT_TARGET}) + ELSE() + SET(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION 14.0) + ENDIF() # set where in the bundle to put the icns file SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/Resources/ShapeWorksStudio.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) diff --git a/Studio/Resources/Info.plist.in b/Studio/Resources/Info.plist.in index e01a7fb5090..9496c0c12a8 100644 --- a/Studio/Resources/Info.plist.in +++ b/Studio/Resources/Info.plist.in @@ -30,6 +30,8 @@ ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped + LSMinimumSystemVersion + ${MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION} LSRequiresCarbon NSHumanReadableCopyright diff --git a/Studio/main.cpp b/Studio/main.cpp index 50175efd46d..46cbadf23e2 100644 --- a/Studio/main.cpp +++ b/Studio/main.cpp @@ -112,6 +112,13 @@ int main(int argc, char** argv) { SW_LOG("ShapeWorks Studio " SHAPEWORKS_VERSION " initializing..."); OverrideQApplication app(argc, argv); + + std::string os_error = ShapeWorksUtils::check_minimum_os_version(); + if (!os_error.empty()) { + SW_ERROR("{}", os_error); + QMessageBox::critical(nullptr, "ShapeWorks Studio", QString::fromStdString(os_error)); + return 1; + } if (PlatformUtils::is_windows()) { app.setStyle(QStyleFactory::create("Fusion")); } diff --git a/Support/osx_distribution.xml b/Support/osx_distribution.xml new file mode 100644 index 00000000000..d0dc80d497d --- /dev/null +++ b/Support/osx_distribution.xml @@ -0,0 +1,30 @@ + + + + ShapeWorks + + + + + + + + + + + + + ShapeWorks-component.pkg + diff --git a/Support/package.sh b/Support/package.sh index da5a166e431..934592b0547 100755 --- a/Support/package.sh +++ b/Support/package.sh @@ -237,7 +237,21 @@ if [[ "$OSTYPE" == "darwin"* ]]; then cp ${ROOT}/docs/users/Mac_README.txt ${VERSION}/README.txt pkgbuild --quiet --analyze --root ${VERSION} ShapeWorks.plist plutil -replace BundleIsRelocatable -bool NO ShapeWorks.plist - pkgbuild --component-plist ShapeWorks.plist --install-location /Applications/ShapeWorks --root ${VERSION} --identifier edu.utah.sci.shapeworks ${ROOT}/artifacts/${VERSION}.pkg --scripts ${ROOT}/Support/osxscripts + PKG_TMP=$(mktemp -d) + pkgbuild --component-plist ShapeWorks.plist --install-location /Applications/ShapeWorks --root ${VERSION} --identifier edu.utah.sci.shapeworks ${PKG_TMP}/ShapeWorks-component.pkg --scripts ${ROOT}/Support/osxscripts || exit 1 + + # Wrap it in a distribution package so the Installer refuses older macOS + # with a clear message instead of installing something that won't run. + MIN_MACOS=${MACOSX_DEPLOYMENT_TARGET:-14.0} + if [[ "$(uname -m)" == "arm64" ]]; then + HOST_ARCHS="arm64" + else + HOST_ARCHS="x86_64,arm64" + fi + sed -e "s/MIN_MACOS/${MIN_MACOS}/g" -e "s/HOST_ARCHS/${HOST_ARCHS}/g" \ + ${ROOT}/Support/osx_distribution.xml > ${PKG_TMP}/distribution.xml + productbuild --distribution ${PKG_TMP}/distribution.xml --package-path ${PKG_TMP} ${ROOT}/artifacts/${VERSION}.pkg || exit 1 + rm -rf ${PKG_TMP} fi cd $ROOT diff --git a/cmake/InstallBundledPython.cmake b/cmake/InstallBundledPython.cmake index bccf19baadc..a8ff9b030fd 100644 --- a/cmake/InstallBundledPython.cmake +++ b/cmake/InstallBundledPython.cmake @@ -491,6 +491,14 @@ file(MAKE_DIRECTORY "${_swpython_dir}") if(APPLE) file(WRITE "${_swpython_dir}/swpython" [=[#!/bin/bash +# Keep in sync with CMAKE_OSX_DEPLOYMENT_TARGET: older macOS fails with cryptic +# symbol errors (e.g. numpy's Accelerate build) instead of a clear message. +SW_MIN_MACOS=14 +_sw_os="$(sw_vers -productVersion 2>/dev/null)" +if [ -n "${_sw_os}" ] && [ "${_sw_os%%.*}" -lt "${SW_MIN_MACOS}" ]; then + echo "ShapeWorks requires macOS ${SW_MIN_MACOS} or later. This Mac is running macOS ${_sw_os}." >&2 + exit 1 +fi LOC="`dirname "$0"`" PYTHONHOME="${LOC}/ShapeWorksStudio.app/Contents/Resources/Python" PATH="${LOC}:${PATH}" diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index bab1bb8da88..0a7054d9146 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -12,6 +12,9 @@ * New `swpython` and `swpip` wrappers run scripts against the bundled Python and install additional packages into a persistent per-user site-packages directory. Windows installs add a **ShapeWorks Prompt** Start Menu shortcut that puts these on `PATH`. * PyTorch installs on demand the first time DeepSSM runs (~3 GB, one-time) via `light-the-torch`, into a per-user directory that survives reinstalls. + * **Platform support** + * macOS 14 (Sonoma) or later is now required (6.7 supported macOS 12). The installer, Studio, the `shapeworks` CLI and `swpython` refuse older macOS with a clear message. + * **ShapeWorks Back-end** * Registration-based particle initialization as an alternative to particle splitting: particles are spread over a single reference shape and carried onto every other shape by deformable registration (rigid → affine → SyN over distance transforms), so each shape starts optimization already holding a full set of corresponding particles (#2374) * New `correspondence-quality` command and Python API that scores each subject by reconstructing its surface from its local particles and measuring distance back to the groomed mesh, normalized by bounding-box diagonal for comparison across anatomies (#2612) diff --git a/docs/users/Mac_README.txt b/docs/users/Mac_README.txt index aba5d388d6f..4bf38ec183e 100644 --- a/docs/users/Mac_README.txt +++ b/docs/users/Mac_README.txt @@ -20,6 +20,8 @@ Welcome to ShapeWorks! ShapeWorks 6.8 ships with a complete Python environment built in. There is no conda environment to set up. +ShapeWorks requires macOS 14 (Sonoma) or later. + 1. Install the ShapeWorks .pkg file by double-clicking on the file. It installs into /Applications/ShapeWorks/. diff --git a/docs/users/install.md b/docs/users/install.md index 455f9ee74f9..d008ffdaeeb 100644 --- a/docs/users/install.md +++ b/docs/users/install.md @@ -111,6 +111,11 @@ This installs to your per-user ShapeWorks site-packages and persists across sess ## Installing ShapeWorks on Mac +!!! important "Requires macOS 14 (Sonoma) or later" + ShapeWorks 6.8 supports macOS 14 or later on both Apple Silicon and Intel + Macs. The installer will not install on older versions of macOS. To check + your version, choose **Apple menu > About This Mac**. + Please make sure that you download the [latest ShapeWorks binary release](https://github.com/SCIInstitute/ShapeWorks/releases/latest), or up-to-date development builds from the master branch for [Mac](https://github.com/SCIInstitute/ShapeWorks/releases/tag/dev-mac) *(remember these are in-progress development builds, not official releases).* 1. Install the ShapeWorks `.pkg` file by double-clicking it. It installs into From 412b0f9ea13e3177c0f608a2904b81bc24602e62 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 09:58:46 -0600 Subject: [PATCH 6/7] Report DeepSSM as unsupported on Intel Macs The last PyTorch with Intel macOS builds (2.2) was built against numpy 1.x and can't exchange arrays with the bundled numpy 2.x, so DeepSSM would install torch and then fail partway through. Studio, the deepssm CLI command and ensure_torch() now stop with a clear message on Intel Macs before downloading anything. Document the limitation. --- Applications/shapeworks/Commands.cpp | 6 ++++++ Libs/Application/Job/PythonWorker.cpp | 17 +++++++++++++++++ Libs/Application/Job/PythonWorker.h | 3 +++ Python/shapeworks/shapeworks/torch_install.py | 14 ++++++++++++++ Studio/DeepSSM/DeepSSMTool.cpp | 6 ++++++ docs/about/release-notes.md | 1 + docs/deep-learning/deep-ssm.md | 2 ++ docs/users/install.md | 4 ++++ 8 files changed, 53 insertions(+) diff --git a/Applications/shapeworks/Commands.cpp b/Applications/shapeworks/Commands.cpp index 768d654a52b..d70df188204 100644 --- a/Applications/shapeworks/Commands.cpp +++ b/Applications/shapeworks/Commands.cpp @@ -592,6 +592,12 @@ bool DeepSSMCommand::execute(const optparse::Values& options, SharedCommandData& ProjectHandle project = std::make_shared(); project->load(project_file); + std::string unsupported = PythonWorker::deepssm_unsupported_reason(); + if (!unsupported.empty()) { + std::cerr << "Error: " << unsupported << "\n"; + return false; + } + // Check if PyTorch is available; install if needed if (!PythonWorker::is_torch_available()) { std::cout << "PyTorch is not installed. Installing via light-the-torch...\n"; diff --git a/Libs/Application/Job/PythonWorker.cpp b/Libs/Application/Job/PythonWorker.cpp index 74f227d4665..1a944424e43 100644 --- a/Libs/Application/Job/PythonWorker.cpp +++ b/Libs/Application/Job/PythonWorker.cpp @@ -488,6 +488,17 @@ std::string PythonWorker::get_user_site_packages() { return path.toStdString(); } +//--------------------------------------------------------------------------- +std::string PythonWorker::deepssm_unsupported_reason() { +#if defined(__APPLE__) && defined(__x86_64__) + // The last Intel macOS torch (2.2) was built against numpy 1.x and can't + // exchange arrays with the bundled numpy 2.x, so DeepSSM would fail later. + return "DeepSSM is not supported on Intel Macs: PyTorch no longer publishes Intel macOS builds that work with ShapeWorks' bundled Python. Please use an Apple Silicon Mac, Windows, or Linux for DeepSSM."; +#else + return ""; +#endif +} + //--------------------------------------------------------------------------- bool PythonWorker::is_torch_available(int timeout_ms) { #ifdef SHAPEWORKS_BUNDLED_PYTHON @@ -540,6 +551,12 @@ bool PythonWorker::is_torch_available(int timeout_ms) { //--------------------------------------------------------------------------- bool PythonWorker::install_torch(std::function output_callback) { + std::string unsupported = deepssm_unsupported_reason(); + if (!unsupported.empty()) { + if (output_callback) output_callback("Error: " + unsupported); + return false; + } + #ifdef SHAPEWORKS_BUNDLED_PYTHON std::string python_home = find_bundled_python_home(); if (python_home.empty()) { diff --git a/Libs/Application/Job/PythonWorker.h b/Libs/Application/Job/PythonWorker.h index c2c03e89b1d..e1b1742882d 100644 --- a/Libs/Application/Job/PythonWorker.h +++ b/Libs/Application/Job/PythonWorker.h @@ -40,6 +40,9 @@ class PythonWorker : public QObject { void abort_job(); + //! Returns why DeepSSM can't run on this platform, or "" if it can. + static std::string deepssm_unsupported_reason(); + //! Check if PyTorch is available in the bundled Python. //! Returns true if torch can be imported successfully. static bool is_torch_available(int timeout_ms = 10000); diff --git a/Python/shapeworks/shapeworks/torch_install.py b/Python/shapeworks/shapeworks/torch_install.py index 5e064c38a6a..249b4d1ee4e 100644 --- a/Python/shapeworks/shapeworks/torch_install.py +++ b/Python/shapeworks/shapeworks/torch_install.py @@ -38,6 +38,15 @@ def _add_user_site_to_path(): sys.path.insert(0, user_site) +_INTEL_MAC_MESSAGE = ( + "DeepSSM is not supported on Intel Macs: PyTorch no longer publishes Intel macOS builds that work with ShapeWorks' bundled Python. Please use an Apple Silicon Mac, Windows, or Linux for DeepSSM." +) + + +def _is_intel_mac(): + return platform.system() == "Darwin" and platform.machine() == "x86_64" + + def is_torch_available(): """Return True if torch can be imported (after adding the user-site path).""" _add_user_site_to_path() @@ -55,6 +64,11 @@ def ensure_torch(): ShapeWorks Studio and `shapeworks deepssm` use, via light-the-torch (which selects the correct CUDA/CPU wheel for the host). """ + # The last Intel macOS torch (2.2) was built against numpy 1.x and can't + # exchange arrays with the bundled numpy 2.x, so DeepSSM would fail later. + if _is_intel_mac(): + raise RuntimeError(_INTEL_MAC_MESSAGE) + if is_torch_available(): return diff --git a/Studio/DeepSSM/DeepSSMTool.cpp b/Studio/DeepSSM/DeepSSMTool.cpp index c5a5c3f3cc1..56b5795269c 100644 --- a/Studio/DeepSSM/DeepSSMTool.cpp +++ b/Studio/DeepSSM/DeepSSMTool.cpp @@ -875,6 +875,12 @@ void DeepSSMTool::restore_defaults() { //--------------------------------------------------------------------------- void DeepSSMTool::run_tool(DeepSSMJob::JobType job_type) { + std::string unsupported = PythonWorker::deepssm_unsupported_reason(); + if (!unsupported.empty()) { + QMessageBox::warning(this, "DeepSSM Not Supported", QString::fromStdString(unsupported)); + return; + } + // Check if PyTorch is available; if not, offer to install it if (!PythonWorker::is_torch_available()) { auto reply = QMessageBox::question(this, "PyTorch Required", diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 0a7054d9146..1594ceeb6b0 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -14,6 +14,7 @@ * **Platform support** * macOS 14 (Sonoma) or later is now required (6.7 supported macOS 12). The installer, Studio, the `shapeworks` CLI and `swpython` refuse older macOS with a clear message. + * DeepSSM is not supported on Intel Macs: PyTorch no longer publishes Intel macOS builds compatible with the bundled Python. Studio, the CLI and `ensure_torch()` say so instead of failing partway through. * **ShapeWorks Back-end** * Registration-based particle initialization as an alternative to particle splitting: particles are spread over a single reference shape and carried onto every other shape by deformable registration (rigid → affine → SyN over distance transforms), so each shape starts optimization already holding a full set of corresponding particles (#2374) diff --git a/docs/deep-learning/deep-ssm.md b/docs/deep-learning/deep-ssm.md index 387ab655f9a..4bc65a3e6f2 100644 --- a/docs/deep-learning/deep-ssm.md +++ b/docs/deep-learning/deep-ssm.md @@ -7,6 +7,8 @@ DeepSSM is a deep learning framework that estimates statistical representations - Riddhish Bhalodia, Shireen Elhabian, Ladislav Kavan, and Ross Whitaker. DeepSSM: a deep learning framework for statistical shape modeling from raw images. In MICCAI-ShapeMI, pp. 244-257. Springer, Cham, 2018. - Riddhish Bhalodia, Anupama Goparaju, Tim Sodergren, Alan Morris, Evgueni Kholmovski, Nassir Marrouche, Joshua Cates, Ross Whitaker, Shireen Elhabian. Deep Learning for End-to-End Atrial Fibrillation Recurrence Estimation. Computing in Cardiology (CinC), 2018. +!!! warning "Not supported on Intel Macs" + PyTorch no longer publishes Intel macOS builds that work with ShapeWorks, so DeepSSM runs on Apple Silicon Macs, Windows, and Linux only. ## What is DeepSSM? diff --git a/docs/users/install.md b/docs/users/install.md index d008ffdaeeb..72f4698bd54 100644 --- a/docs/users/install.md +++ b/docs/users/install.md @@ -116,6 +116,10 @@ This installs to your per-user ShapeWorks site-packages and persists across sess Macs. The installer will not install on older versions of macOS. To check your version, choose **Apple menu > About This Mac**. + DeepSSM is not supported on Intel Macs, because PyTorch no longer + publishes Intel macOS builds that work with ShapeWorks. Everything else + works on Intel Macs. Use an Apple Silicon Mac, Windows, or Linux for DeepSSM. + Please make sure that you download the [latest ShapeWorks binary release](https://github.com/SCIInstitute/ShapeWorks/releases/latest), or up-to-date development builds from the master branch for [Mac](https://github.com/SCIInstitute/ShapeWorks/releases/tag/dev-mac) *(remember these are in-progress development builds, not official releases).* 1. Install the ShapeWorks `.pkg` file by double-clicking it. It installs into From dbedfd877668ed993fc4ac057439f65124b1500d Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 30 Oct 2026 15:06:42 -0600 Subject: [PATCH 7/7] Skip DeepSSM tests on Intel Macs DeepSSM is unsupported there, so the tests now skip instead of failing. They passed in CI only because the conda test env installs numpy<2 on Intel Macs, unlike the shipped bundle. --- Testing/DeepSSMTests/DeepSSMTests.cpp | 4 ++++ Testing/UseCaseTests/UseCaseTests.cpp | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Testing/DeepSSMTests/DeepSSMTests.cpp b/Testing/DeepSSMTests/DeepSSMTests.cpp index 6783e325b90..44ae7b8ddff 100644 --- a/Testing/DeepSSMTests/DeepSSMTests.cpp +++ b/Testing/DeepSSMTests/DeepSSMTests.cpp @@ -4,6 +4,10 @@ using namespace shapeworks; //--------------------------------------------------------------------------- void run_deepssm_test(const std::string& name) { +#if defined(__APPLE__) && defined(__x86_64__) + // DeepSSM is unsupported on Intel Macs (see PythonWorker::deepssm_unsupported_reason) + GTEST_SKIP() << "DeepSSM is not supported on Intel Macs"; +#endif setupenv(std::string(TEST_DATA_DIR) + "/../DeepSSMTests"); std::string command = "bash " + name; diff --git a/Testing/UseCaseTests/UseCaseTests.cpp b/Testing/UseCaseTests/UseCaseTests.cpp index ca8ee646dca..92b1332eba0 100644 --- a/Testing/UseCaseTests/UseCaseTests.cpp +++ b/Testing/UseCaseTests/UseCaseTests.cpp @@ -60,7 +60,13 @@ TEST(UseCaseTests, left_atrium) { run_test("left_atrium"); } TEST(UseCaseTests, femur_cut) { run_test("femur_cut"); } //--------------------------------------------------------------------------- -TEST(UseCaseTests, deep_ssm) { run_test("deep_ssm"); } +TEST(UseCaseTests, deep_ssm) { +#if defined(__APPLE__) && defined(__x86_64__) + // DeepSSM is unsupported on Intel Macs (see PythonWorker::deepssm_unsupported_reason) + GTEST_SKIP() << "DeepSSM is not supported on Intel Macs"; +#endif + run_test("deep_ssm"); +} //--------------------------------------------------------------------------- TEST(UseCaseTests, supershapes_1mode_contour) { run_test("supershapes_1mode_contour"); }