Skip to content

Instantly share code, notes, and snippets.

@danwald
Last active May 15, 2026 03:28
Show Gist options
  • Select an option

  • Save danwald/c566caf19a8ea06ca818c26ba0f4d477 to your computer and use it in GitHub Desktop.

Select an option

Save danwald/c566caf19a8ea06ca818c26ba0f4d477 to your computer and use it in GitHub Desktop.
Build hugin (2025) from source
#!/usr/bin/env bash
# build-hugin-macos.sh — Build Hugin 2025.0.1 from source on macOS Apple Silicon
#
# Usage:
# bash build-hugin-macos.sh
#
# Output: Hugin-2025.0.1-arm64.dmg in the current directory
# Install: open the DMG and drag Hugin.app to /Applications
#
# Tested on: macOS 15+ (Apple Silicon, M1/M2/M3/M4)
# Requirements: Homebrew, Xcode Command Line Tools
set -euo pipefail
HUGIN_VERSION="2025.0.1"
TARBALL="hugin-${HUGIN_VERSION}.tar.bz2"
TARBALL_URL="https://sourceforge.net/projects/hugin/files/hugin/hugin-2025.0/${TARBALL}/download"
SRCDIR="hugin-${HUGIN_VERSION}"
BUILDDIR="build"
PATCHFILE="hugin-${HUGIN_VERSION}-macos.patch"
DMG="Hugin-${HUGIN_VERSION}-arm64.dmg"
log() { echo "==> $*"; }
# ── 1. Preflight checks ──────────────────────────────────────────────────────
log "Checking prerequisites..."
command -v brew >/dev/null || { echo "ERROR: Homebrew not found — install from https://brew.sh"; exit 1; }
command -v cmake >/dev/null || { echo "ERROR: cmake not found (brew install cmake)"; exit 1; }
command -v git >/dev/null || { echo "ERROR: git not found"; exit 1; }
command -v curl >/dev/null || { echo "ERROR: curl not found"; exit 1; }
# ── 2. Download source tarball ───────────────────────────────────────────────
if [ ! -f "$TARBALL" ]; then
log "Downloading $TARBALL from SourceForge (~70 MB)..."
curl -L --progress-bar -o "$TARBALL" "$TARBALL_URL"
else
log "Source tarball already present, skipping download."
fi
# ── 3. Write patch file ──────────────────────────────────────────────────────
log "Writing patch file..."
cat > "$PATCHFILE" << 'ENDOFPATCH'
--- hugin-orig-extract/hugin-2025.0.1/CMakeLists.txt 2025-12-10 22:35:48
+++ hugin-modified/CMakeLists.txt 2026-05-12 21:09:33
@@ -19,7 +19,8 @@
endif()
if(APPLE)
- set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9")
+ # AIDEV-NOTE: bumped from 10.9 to 10.15; std::filesystem requires 10.15+
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
if (MAC_SELF_CONTAINED_BUNDLE)
set(CMAKE_LIBRARY_PATH ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/lib)
set(CMAKE_INCLUDE_PATH ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/include ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/bin)
--- hugin-orig-extract/hugin-2025.0.1/CMakeModules/FindVIGRA.cmake 2021-08-21 18:43:52
+++ hugin-modified/CMakeModules/FindVIGRA.cmake 2026-05-12 21:09:33
@@ -27,15 +27,16 @@
)
ELSE(WIN32)
FIND_PATH(VIGRA_INCLUDE_DIR vigra/gaborfilter.hxx
+ /opt/homebrew/include
/usr/local/include
/usr/include
/opt/local/include
)
FIND_LIBRARY(VIGRA_LIBRARIES
- NAMES vigraimpex libvigraimpex
- PATHS /usr/lib /usr/local/lib /opt/local/lib
- )
+ NAMES vigraimpex libvigraimpex
+ PATHS /opt/homebrew/lib /usr/lib /usr/local/lib /opt/local/lib
+ )
ENDIF(WIN32)
@@ -53,9 +54,15 @@
IF(NOT VIGRA_CONFIG_VERSION_HXX)
MESSAGE(FATAL_ERROR "Could not find vigra/configVersion.hxx or vigra/config_version.hxx. Your vigra installation seems to be corrupt.")
ENDIF()
- FILE(STRINGS "${VIGRA_CONFIG_VERSION_HXX}" VIGRA_VERSION_HXX REGEX ".*#define +VIGRA_VERSION +\"")
- STRING(REGEX REPLACE ".*#define +VIGRA_VERSION +\"([.0-9]+).*" "\\1" VIGRA_VERSION "${VIGRA_VERSION_HXX}")
- IF(${VIGRA_VERSION} VERSION_EQUAL VIGRA_FIND_VERSION OR ${VIGRA_VERSION} VERSION_GREATER VIGRA_FIND_VERSION)
+ # AIDEV-NOTE: config_version.hxx uses macro-based version (not a string literal); extract major/minor/patch individually
+ FILE(STRINGS "${VIGRA_CONFIG_VERSION_HXX}" VIGRA_VERSION_MAJOR_LINE REGEX "#define +VIGRA_VERSION_MAJOR +[0-9]+")
+ FILE(STRINGS "${VIGRA_CONFIG_VERSION_HXX}" VIGRA_VERSION_MINOR_LINE REGEX "#define +VIGRA_VERSION_MINOR +[0-9]+")
+ FILE(STRINGS "${VIGRA_CONFIG_VERSION_HXX}" VIGRA_VERSION_PATCH_LINE REGEX "#define +VIGRA_VERSION_PATCH +[0-9]+")
+ STRING(REGEX REPLACE ".*#define +VIGRA_VERSION_MAJOR +([0-9]+).*" "\\1" VIGRA_VERSION_MAJOR "${VIGRA_VERSION_MAJOR_LINE}")
+ STRING(REGEX REPLACE ".*#define +VIGRA_VERSION_MINOR +([0-9]+).*" "\\1" VIGRA_VERSION_MINOR "${VIGRA_VERSION_MINOR_LINE}")
+ STRING(REGEX REPLACE ".*#define +VIGRA_VERSION_PATCH +([0-9]+).*" "\\1" VIGRA_VERSION_PATCH "${VIGRA_VERSION_PATCH_LINE}")
+ SET(VIGRA_VERSION "${VIGRA_VERSION_MAJOR}.${VIGRA_VERSION_MINOR}.${VIGRA_VERSION_PATCH}")
+ IF("${VIGRA_VERSION}" VERSION_GREATER_EQUAL "${VIGRA_FIND_VERSION}")
SET(VIGRA_VERSION_CHECK TRUE)
MESSAGE(STATUS "VIGRA version: ${VIGRA_VERSION}")
ELSE()
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/base_wx/Executor.cpp 2025-09-16 18:55:30
+++ hugin-modified/src/hugin1/base_wx/Executor.cpp 2026-05-14 22:16:59
@@ -128,19 +128,9 @@
// return path in internal program (program that is shipped with Hugin)
wxString GetInternalProgram(const wxString& bindir, const wxString& name)
{
-#if defined __WXMAC__ && defined MAC_SELF_CONTAINED_BUNDLE
- CFStringRef filename = MacCreateCFStringWithWxString(name);
- wxString fn = MacGetPathToBundledExecutableFile(filename);
- CFRelease(filename);
- if (fn == wxEmptyString)
- {
- std::cerr << wxString::Format(_("External program %s not found in the bundle, reverting to system path"), name.c_str()) << std::endl;
- return name;
- }
- return fn;
-#else
+ // AIDEV-NOTE: Use bindir directly on all platforms — avoids CF bundle lookup
+ // failures in child processes (e.g. hugin_executor) on macOS.
return bindir + name;
-#endif
};
// return name of external program (can be program bundeled with Hugin, or an external program
@@ -174,15 +164,11 @@
};
};
- CFStringRef filename = MacCreateCFStringWithWxString(name);
- wxString fn = MacGetPathToBundledExecutableFile(filename);
- CFRelease(filename);
- if (fn == wxEmptyString)
- {
- std::cerr << wxString::Format(_("WARNING: External program %s not found in the bundle, reverting to system path"), name.c_str()) << std::endl;
- return name;
- };
- return fn;
+ // AIDEV-NOTE: Use bindir (Contents/MacOS/) directly — reliable for both the main
+ // Hugin process and child processes (e.g. hugin_executor) where
+ // CFBundleCopyAuxiliaryExecutableURL fails. All external tools are co-located
+ // with the Hugin executable in Contents/MacOS/.
+ return bindir + name;
#else
if (config->Read(name + "/Custom", 0l))
{
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/executor/hugin_executor.cpp 2025-09-16 18:57:59
+++ hugin-modified/src/hugin1/executor/hugin_executor.cpp 2026-05-14 22:24:15
@@ -95,6 +95,21 @@
setlocale(LC_ALL, "");
// initialize i18n
int localeID = config->Read("language", (long)HUGIN_LANGUAGE);
+ // AIDEV-NOTE: hugin_executor uses wxApp on macOS (can show dialogs), so the
+ // same locale pre-check as huginApp.cpp is needed here. Without it, an
+ // unsupported system locale (e.g. en_AE) causes an invisible blocking modal.
+ if (localeID == wxLANGUAGE_DEFAULT || localeID == wxLANGUAGE_UNKNOWN)
+ {
+ int sysLang = wxLocale::GetSystemLanguage();
+ const wxLanguageInfo* info = wxLocale::GetLanguageInfo(sysLang);
+ if (info)
+ {
+ wxString name = info->CanonicalName + ".UTF-8";
+ if (!setlocale(LC_ALL, name.mb_str()))
+ localeID = wxLANGUAGE_ENGLISH_US;
+ setlocale(LC_ALL, ""); // restore
+ }
+ }
m_locale.Init(localeID);
// set the name of locale recource to look for
m_locale.AddCatalog("hugin");
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/hugin/huginApp.cpp 2025-10-27 20:56:09
+++ hugin-modified/src/hugin1/hugin/huginApp.cpp 2026-05-12 21:10:01
@@ -300,6 +300,25 @@
int localeID = config->Read("language", (long) HUGIN_LANGUAGE);
DEBUG_TRACE("localeID: " << localeID);
{
+ // AIDEV-NOTE: wxLocale::Init() shows an error dialog if the system locale
+ // (e.g. en_AE on UAE-region systems) is unknown to the C runtime.
+ // Pre-check via setlocale() and silently fall back to English US so the
+ // dialog never appears. Translations still work for explicitly chosen languages.
+ if (localeID == wxLANGUAGE_DEFAULT || localeID == wxLANGUAGE_UNKNOWN)
+ {
+ int sysLang = wxLocale::GetSystemLanguage();
+ const wxLanguageInfo* info = wxLocale::GetLanguageInfo(sysLang);
+ if (info)
+ {
+ wxString name = info->CanonicalName + ".UTF-8";
+ if (!setlocale(LC_ALL, name.mb_str()))
+ {
+ // System locale not supported by C runtime; use English US silently.
+ localeID = wxLANGUAGE_ENGLISH_US;
+ }
+ setlocale(LC_ALL, ""); // restore
+ }
+ }
bool bLInit;
bLInit = locale.Init(localeID);
if (bLInit) {
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/ptbatcher/CMakeLists.txt 2025-12-03 20:18:53
+++ hugin-modified/src/hugin1/ptbatcher/CMakeLists.txt 2026-05-12 21:09:33
@@ -29,11 +29,9 @@
IF(MAC_SELF_CONTAINED_BUNDLE)
- # Tools
+ # AIDEV-NOTE: enblend/enfuse removed — not available via Homebrew; install separately if needed
set( TOOLS ${CMAKE_BINARY_DIR}/src/tools/align_image_stack ${CMAKE_BINARY_DIR}/src/tools/nona
${CMAKE_BINARY_DIR}/src/tools/hugin_hdrmerge ${CMAKE_BINARY_DIR}/src/tools/verdandi
- ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/bin/enblend
- ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/bin/enfuse
)
FOREACH(_file ${TOOLS})
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/ptbatcher/PTBatcherGUI.cpp 2025-10-08 23:00:21
+++ hugin-modified/src/hugin1/ptbatcher/PTBatcherGUI.cpp 2026-05-14 22:30:46
@@ -73,7 +73,21 @@
int localeID = wxConfigBase::Get()->Read("language", (long) wxLANGUAGE_DEFAULT);
m_locale.Init(localeID);
#else
- m_locale.Init(wxLANGUAGE_DEFAULT);
+ // AIDEV-NOTE: pre-check locale support to avoid invisible blocking modal on
+ // systems with unsupported locales (e.g. en_AE). Same fix as huginApp.cpp.
+ {
+ int localeID = wxLANGUAGE_DEFAULT;
+ int sysLang = wxLocale::GetSystemLanguage();
+ const wxLanguageInfo* info = wxLocale::GetLanguageInfo(sysLang);
+ if (info)
+ {
+ wxString localeName = info->CanonicalName + ".UTF-8";
+ if (!setlocale(LC_ALL, localeName.mb_str()))
+ localeID = wxLANGUAGE_ENGLISH_US;
+ setlocale(LC_ALL, "");
+ }
+ m_locale.Init(localeID);
+ }
#endif
// initialize help provider
wxHelpControllerHelpProvider* provider = new wxHelpControllerHelpProvider;
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/stitch_project/CMakeLists.txt 2025-10-02 20:18:43
+++ hugin-modified/src/hugin1/stitch_project/CMakeLists.txt 2026-05-12 21:09:33
@@ -21,11 +21,9 @@
IF(MAC_SELF_CONTAINED_BUNDLE)
- # Tools
+ # AIDEV-NOTE: enblend/enfuse removed — not available via Homebrew; install separately if needed
set( TOOLS ${CMAKE_BINARY_DIR}/src/tools/align_image_stack ${CMAKE_BINARY_DIR}/src/tools/nona
${CMAKE_BINARY_DIR}/src/tools/hugin_hdrmerge ${CMAKE_BINARY_DIR}/src/tools/verdandi
- ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/bin/enblend
- ${CMAKE_SOURCE_DIR}/mac/ExternalPrograms/repository/bin/enfuse
)
FOREACH(_file ${TOOLS})
--- hugin-orig-extract/hugin-2025.0.1/src/hugin1/stitch_project/hugin_stitch_project.cpp 2025-12-03 20:18:53
+++ hugin-modified/src/hugin1/stitch_project/hugin_stitch_project.cpp 2026-05-14 22:30:46
@@ -313,7 +313,21 @@
int localeID = wxConfigBase::Get()->Read("language", (long) wxLANGUAGE_DEFAULT);
m_locale.Init(localeID);
#else
- m_locale.Init(wxLANGUAGE_DEFAULT);
+ // AIDEV-NOTE: pre-check locale support to avoid invisible blocking modal on
+ // systems with unsupported locales (e.g. en_AE). Same fix as huginApp.cpp.
+ {
+ int localeID = wxLANGUAGE_DEFAULT;
+ int sysLang = wxLocale::GetSystemLanguage();
+ const wxLanguageInfo* info = wxLocale::GetLanguageInfo(sysLang);
+ if (info)
+ {
+ wxString localeName = info->CanonicalName + ".UTF-8";
+ if (!setlocale(LC_ALL, localeName.mb_str()))
+ localeID = wxLANGUAGE_ENGLISH_US;
+ setlocale(LC_ALL, "");
+ }
+ m_locale.Init(localeID);
+ }
#endif
// setup the environment for the different operating systems
ENDOFPATCH
# ── 4. Homebrew dependencies ─────────────────────────────────────────────────
log "Installing Homebrew dependencies..."
brew install cmake pkg-config libtiff jpeg-turbo libpng exiv2 libpano \
little-cms2 boost libomp fftw wxwidgets glew libepoxy \
gsl exiftool 2>/dev/null || true
# ── 5. Build VIGRA from source (not in Homebrew) ─────────────────────────────
VIGRA_PREFIX="/opt/homebrew"
if [ ! -f "${VIGRA_PREFIX}/include/vigra/gaborfilter.hxx" ]; then
log "Building VIGRA from source (~2 min)..."
VIGRA_SRC="/tmp/vigra-src"
VIGRA_BUILD="/tmp/vigra-build"
rm -rf "$VIGRA_SRC" "$VIGRA_BUILD"
git clone --depth 1 https://github.com/ukoethe/vigra.git "$VIGRA_SRC"
mkdir -p "$VIGRA_BUILD"
cmake -S "$VIGRA_SRC" -B "$VIGRA_BUILD" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$VIGRA_PREFIX" \
-DWITH_OPENEXR=OFF \
-DWITH_HDF5=OFF \
-DWITH_VIGRANUMPY=OFF \
-DDOCINSTALL=OFF
make -C "$VIGRA_BUILD" -j"$(sysctl -n hw.logicalcpu)"
make -C "$VIGRA_BUILD" install
log "VIGRA installed."
else
log "VIGRA already installed, skipping."
fi
# ── 6. Build enblend/enfuse from source (not in Homebrew) ────────────────────
ENBLEND_BIN="/tmp/enblend-build/bin/enblend"
if [ ! -x "$ENBLEND_BIN" ]; then
log "Building enblend/enfuse from source (~3 min)..."
ENBLEND_SRC="/tmp/enblend-src"
ENBLEND_BUILD="/tmp/enblend-build"
rm -rf "$ENBLEND_SRC" "$ENBLEND_BUILD"
git clone --depth 1 https://github.com/jackmitch/enblend-enfuse.git "$ENBLEND_SRC"
mkdir -p "$ENBLEND_BUILD"
cmake -S "$ENBLEND_SRC" -B "$ENBLEND_BUILD" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$(brew --prefix)" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
-DENABLE_OPENCL=OFF \
-DENABLE_OPENMP=OFF \
-DDOC=OFF
make -C "$ENBLEND_BUILD" -j"$(sysctl -n hw.logicalcpu)" enblend enfuse
log "enblend/enfuse built."
else
log "enblend/enfuse already built, skipping."
fi
# ── 7. Extract source ─────────────────────────────────────────────────────────
log "Extracting $TARBALL..."
rm -rf "$SRCDIR"
tar xjf "$TARBALL"
# ── 8. Apply patch ────────────────────────────────────────────────────────────
log "Applying patch..."
patch -p1 -l -d "$SRCDIR" < "$PATCHFILE"
# ── 9. Configure ─────────────────────────────────────────────────────────────
log "Configuring CMake..."
rm -rf "$BUILDDIR"
mkdir -p "$BUILDDIR"
cmake -S "$SRCDIR" -B "$BUILDDIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$(brew --prefix)" \
-DMAC_SELF_CONTAINED_BUNDLE=ON
# ── 10. Build ─────────────────────────────────────────────────────────────────
log "Building Hugin (~5 min)..."
make -C "$BUILDDIR" -j"$(sysctl -n hw.logicalcpu)" \
Hugin PTBatcherGUI HuginStitchProject calibrate_lens_gui
# ── 11. Assemble complete Hugin.app bundle ───────────────────────────────────
log "Assembling app bundle..."
HUGIN_APP="${BUILDDIR}/src/hugin1/hugin/Hugin.app"
MACOS="${HUGIN_APP}/Contents/MacOS"
# Core tools built above but not auto-copied into bundle
for t in nona hugin_hdrmerge verdandi; do
cp "${BUILDDIR}/src/tools/$t" "$MACOS/"
chmod +x "$MACOS/$t"
done
# enblend / enfuse — Mach-O binaries (locale is fixed in source; no wrapper needed)
cp /tmp/enblend-build/bin/enblend "$MACOS/enblend"
cp /tmp/enblend-build/bin/enfuse "$MACOS/enfuse"
chmod +x "$MACOS/enblend" "$MACOS/enfuse"
# exiftool (Perl script from Homebrew)
cp "$(brew --prefix)/bin/exiftool" "$MACOS/"
chmod +x "$MACOS/exiftool"
# Patch Info.plist to set locale env for all child processes
PLIST="${HUGIN_APP}/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" "$PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :LSEnvironment:LANG string en_US.UTF-8" "$PLIST" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :LSEnvironment:LC_ALL string en_US.UTF-8" "$PLIST" 2>/dev/null || true
# ── 12. Verify bundle ─────────────────────────────────────────────────────────
log "Verifying bundle tools..."
REQUIRED=(
enblend enfuse nona hugin_hdrmerge verdandi
align_image_stack autooptimiser cpfind celeste_standalone
fulla pano_modify pano_trafo pto_gen pto_lensstack pto_mask
pto_merge pto_move pto_template pto_var tca_correct vig_optimize
cpclean icpfind linefind geocpset checkpto hugin_stacker
hugin_executor hugin_lensdb deghosting_mask exiftool
)
MISSING=0
for t in "${REQUIRED[@]}"; do
if [ ! -x "$MACOS/$t" ]; then
echo " MISSING: $t"
((MISSING++))
fi
done
[ "$MISSING" -eq 0 ] || { echo "ERROR: $MISSING tools missing from bundle"; exit 1; }
log "All ${#REQUIRED[@]} tools present."
# ── 13. Package DMG ───────────────────────────────────────────────────────────
log "Creating DMG..."
STAGE="$(mktemp -d)"
cp -R "${BUILDDIR}/src/hugin1/hugin/Hugin.app" "$STAGE/"
cp -R "${BUILDDIR}/src/hugin1/ptbatcher/PTBatcherGUI.app" "$STAGE/"
cp -R "${BUILDDIR}/src/hugin1/calibrate_lens/calibrate_lens_gui.app" "$STAGE/"
cp -R "${BUILDDIR}/src/hugin1/stitch_project/HuginStitchProject.app" "$STAGE/"
ln -sf /Applications "$STAGE/Applications"
rm -f "$DMG"
hdiutil create \
-volname "Hugin ${HUGIN_VERSION}" \
-srcfolder "$STAGE" \
-ov -format UDZO \
"$DMG"
rm -rf "$STAGE"
log "Done! DMG: $(pwd)/$DMG"
log "Open the DMG and drag Hugin.app to /Applications to install."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment