Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Applications/shapeworks/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ bool DeepSSMCommand::execute(const optparse::Values& options, SharedCommandData&
ProjectHandle project = std::make_shared<Project>();
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";
Expand Down
7 changes: 7 additions & 0 deletions Applications/shapeworks/shapeworks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

#include <exception>
#include <iostream>
#include "Executable.h"
#include "Commands.h"
#include <Profiling.h>
Expand All @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions Examples/Python/RunUseCase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SOURCE_DIR> <BUILD_DIR>"
"""
Expand Down Expand Up @@ -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 <SOURCE_DIR> <BUILD_DIR>\"")

Expand Down
43 changes: 41 additions & 2 deletions Libs/Application/Job/PythonWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ using namespace pybind11::literals; // to bring in the `_a` literal
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#include <QTemporaryDir>
#include <QThread>
#include <iostream>

Expand Down Expand Up @@ -488,7 +489,18 @@ std::string PythonWorker::get_user_site_packages() {
}

//---------------------------------------------------------------------------
bool PythonWorker::is_torch_available() {
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
std::string python_home = find_bundled_python_home();
if (python_home.empty()) {
Expand Down Expand Up @@ -530,7 +542,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
Expand All @@ -539,6 +551,12 @@ bool PythonWorker::is_torch_available() {

//---------------------------------------------------------------------------
bool PythonWorker::install_torch(std::function<void(std::string)> 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()) {
Expand All @@ -564,9 +582,23 @@ bool PythonWorker::install_torch(std::function<void(std::string)> 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) {
Expand Down Expand Up @@ -597,6 +629,13 @@ bool PythonWorker::install_torch(std::function<void(std::string)> 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
Expand Down
5 changes: 4 additions & 1 deletion Libs/Application/Job/PythonWorker.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ 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();
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.
Expand Down
29 changes: 28 additions & 1 deletion Libs/Common/ShapeworksUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
#include <boost/nowide/utf8_codecvt.hpp>
#include <locale>

#ifdef __APPLE__
#include <Availability.h>
#include <sys/sysctl.h>
#endif

namespace shapeworks {

unsigned ShapeWorksUtils::rng_seed_ = std::chrono::system_clock::now().time_since_epoch().count();
Expand All @@ -27,15 +32,37 @@ 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
// wchar_t on Windows. There every narrow string is converted using the codecvt facet imbued
// 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<wchar_t>));
#endif
}

//-----------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions Libs/Common/ShapeworksUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
35 changes: 30 additions & 5 deletions Python/shapeworks/shapeworks/torch_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,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()
Expand All @@ -53,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

Expand All @@ -62,10 +78,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:
Expand All @@ -75,6 +99,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
6 changes: 6 additions & 0 deletions Studio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions Studio/DeepSSM/DeepSSMTool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions Studio/Resources/Info.plist.in
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION}</string>
<key>LSRequiresCarbon</key>
<true/>
<key>NSHumanReadableCopyright</key>
Expand Down
7 changes: 7 additions & 0 deletions Studio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Expand Down
30 changes: 30 additions & 0 deletions Support/osx_distribution.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Installer wrapper for the ShapeWorks component package (see package.sh).
MIN_MACOS and HOST_ARCHS are filled in by package.sh. -->
<installer-gui-script minSpecVersion="2">
<title>ShapeWorks</title>
<options customize="never" require-scripts="false" hostArchitectures="HOST_ARCHS"/>
<installation-check script="check_os()"/>
<script><![CDATA[
function check_os() {
if (system.compareVersions(system.version.ProductVersion, 'MIN_MACOS') < 0) {
my.result.type = 'Fatal';
my.result.title = 'macOS MIN_MACOS or later is required';
my.result.message = 'ShapeWorks requires macOS MIN_MACOS or later. This Mac is running macOS ' +
system.version.ProductVersion + '.';
return false;
}
return true;
}
]]></script>
<choices-outline>
<line choice="default">
<line choice="edu.utah.sci.shapeworks"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="edu.utah.sci.shapeworks" visible="false">
<pkg-ref id="edu.utah.sci.shapeworks"/>
</choice>
<pkg-ref id="edu.utah.sci.shapeworks">ShapeWorks-component.pkg</pkg-ref>
</installer-gui-script>
16 changes: 15 additions & 1 deletion Support/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading