diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 00000000..b3f3985e --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,62 @@ +name: Build on macOS + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies via Homebrew + run: | + brew install cmake opencv dylibbundler create-dmg + + - name: Verify CMake version + run: cmake --version + + - name: Configure CMake + run: | + cmake -B ${{github.workspace}}/build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DFREEGLUT_COCOA=ON \ + -DHD_CPU_OPTIMIZATION=AUTO + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} -j$(sysctl -n hw.ncpu) + + - name: List built binaries + run: | + echo "Built binaries:" + find ${{github.workspace}}/build/bin -type f -perm +111 2>/dev/null || true + ls -lh ${{github.workspace}}/build/bin/ + + - name: Create App Bundle + run: | + chmod +x ${{github.workspace}}/macos_bundle_toolkit/create_dir_bundle.sh + cd ${{github.workspace}} + ./macos_bundle_toolkit/create_dir_bundle.sh + + - name: Create DMG Package + run: | + chmod +x ${{github.workspace}}/macos_bundle_toolkit/create_dir_bundle_dmg.sh + cd ${{github.workspace}} + ./macos_bundle_toolkit/create_dir_bundle_dmg.sh + + - name: Upload DMG Artifact + uses: actions/upload-artifact@v4 + with: + name: hdmapping-macos-dmg + path: ${{github.workspace}}/build/HDMapping.dmg + retention-days: 90 + compression-level: 0 diff --git a/CMakeLists.txt b/CMakeLists.txt index d4145e08..205d2156 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,7 +86,12 @@ else() set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2 -march=x86-64 -mtune=intel -DNDEBUG") message(STATUS "Enabling Intel-optimized build for GCC/Clang") elseif(HD_CPU_OPTIMIZATION STREQUAL "ARM") - if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") + if(APPLE) + # On Apple Silicon (macOS), compiler natively targets ARM64, no -march flag needed + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -DNDEBUG") + message(STATUS "Enabling ARM64 native optimizations for Apple Silicon") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=armv8-a -DNDEBUG") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -march=armv8-a -DNDEBUG") message(STATUS "Enabling ARM64/AArch64 optimizations for GCC/Clang") @@ -98,7 +103,12 @@ else() elseif(HD_CPU_OPTIMIZATION STREQUAL "AUTO") # Auto-detect based on processor cmake_host_system_information(RESULT CPU_VENDOR QUERY PROCESSOR_DESCRIPTION) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") + if(APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64" OR CPU_VENDOR MATCHES "Apple")) + # On Apple Silicon (macOS), compiler natively targets ARM64, no -march flag needed + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG") + set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -DNDEBUG") + message(STATUS "Auto-detected Apple Silicon - enabling native ARM64 optimizations") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=armv8-a -DNDEBUG") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -march=armv8-a -DNDEBUG") diff --git a/macos_bundle_toolkit/bundle_README.txt b/macos_bundle_toolkit/bundle_README.txt new file mode 100644 index 00000000..7756756a --- /dev/null +++ b/macos_bundle_toolkit/bundle_README.txt @@ -0,0 +1,58 @@ +HDMapping Toolset for macOS +============================ + +This directory contains the complete HDMapping toolset with all required +dependencies bundled together. + +INSTALLATION +------------ + +Option 1: Copy to Applications (recommended) + 1. Copy the entire HDMapping folder to /Applications: + cp -r HDMapping /Applications/ + + 2. Add to PATH (optional, for easy command-line access): + echo 'export PATH="/Applications/HDMapping/bin:$PATH"' >> ~/.zshrc + source ~/.zshrc + +Option 2: Keep in any location + Simply copy the HDMapping folder wherever you like. All dependencies + are bundled, so it will work from any location. + +USAGE +----- + +Run tools directly: + ./HDMapping/bin/tool_name + +Or if added to PATH: + tool_name + +Available tools are located in the bin/ directory. Use: + ls ./HDMapping/bin/ + +To see all available commands. + +STRUCTURE +--------- + +HDMapping/ + ├── bin/ All executable tools + ├── lib/ Bundled dynamic libraries (dependencies) + └── README.txt This file + +All required libraries are in the lib/ directory and are automatically +found by the executables using relative paths. + +TROUBLESHOOTING +--------------- + +If you encounter "command not found" errors: + - Make sure you're using the full path to the executable + - Or add HDMapping/bin to your PATH as described above + +If you encounter "library not found" errors: + - Ensure the lib/ directory is in the same parent folder as bin/ + - Do not separate bin/ and lib/ directories + +For more information, visit: https://github.com/MapsHD/HDMapping diff --git a/macos_bundle_toolkit/create_dir_bundle.sh b/macos_bundle_toolkit/create_dir_bundle.sh new file mode 100755 index 00000000..a2052feb --- /dev/null +++ b/macos_bundle_toolkit/create_dir_bundle.sh @@ -0,0 +1,309 @@ +#!/bin/bash + +# Script to create a portable macOS directory bundle for HD Mapping +# This bundles all executables and dependencies into a simple directory structure +# that can be copied to /Applications or any location + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to find a dylib in the build directory +find_dylib_in_build() { + local dylib_name="$1" + + # Search in build/lib first + if [ -e "build/lib/$dylib_name" ]; then + echo "build/lib/$dylib_name" + return + fi + + # Search recursively in build directory (including symlinks) + local found=$(find build -name "$dylib_name" \( -type f -o -type l \) 2>/dev/null | head -1) + if [ -n "$found" ]; then + echo "$found" + return + fi + + echo "" +} + +# Function to copy a dylib and fix its install name +bundle_dylib() { + local dylib_path="$1" + local dylib_name=$(basename "$dylib_path") + local dest="build/HDMapping/lib/$dylib_name" + + # Skip if already copied + if [ -e "$dest" ]; then + return + fi + + # If path doesn't exist, try to find it in build directory + if [ ! -e "$dylib_path" ]; then + dylib_path=$(find_dylib_in_build "$dylib_name") + if [ -z "$dylib_path" ]; then + return + fi + fi + + # If it's a symlink, copy the target and create the symlink + if [ -L "$dylib_path" ]; then + local target=$(readlink "$dylib_path") + local target_name=$(basename "$target") + local dir=$(dirname "$dylib_path") + + # If target is relative, resolve it + if [[ "$target" != /* ]]; then + target="$dir/$target" + fi + + # Bundle the target first + if [ -e "$target" ]; then + bundle_dylib "$target" + fi + + # Create symlink in destination + if [ ! -e "$dest" ] && [ -e "build/HDMapping/lib/$target_name" ]; then + cd "build/HDMapping/lib" + ln -sf "$target_name" "$dylib_name" + cd - > /dev/null + echo " ✓ Linked: $dylib_name -> $target_name" + fi + return + fi + + # Copy the dylib + cp "$dylib_path" "$dest" + chmod +w "$dest" + + # Change its install name to use @loader_path (for dylibs referencing other dylibs) + install_name_tool -id "@loader_path/$dylib_name" "$dest" 2>/dev/null || true + + echo " ✓ Bundled: $dylib_name" + + # Recursively bundle dependencies of this dylib + local deps=$(otool -L "$dylib_path" | grep -v ":" | awk '{print $1}' | grep -v "^/usr/lib" | grep -v "^/System" | grep -v "@rpath" | grep -v "@loader_path") + for dep in $deps; do + local dep_name=$(basename "$dep") + if [[ "$dep" == *".dylib"* ]]; then + # Try the original path first, then search + if [ -f "$dep" ]; then + bundle_dylib "$dep" + else + local found_path=$(find_dylib_in_build "$dep_name") + if [ -n "$found_path" ]; then + bundle_dylib "$found_path" + fi + fi + fi + done +} + +# Function to fix dylib references in a binary +fix_binary_dylibs() { + local binary="$1" + local binary_name=$(basename "$binary") + local is_dylib=false + + # Check if this is a dylib (in lib/) or executable (in bin/) + if [[ "$binary" == *"/lib/"* ]]; then + is_dylib=true + fi + + # Get all non-system dylib dependencies (including @rpath and @loader_path references) + local deps=$(otool -L "$binary" | grep -v ":" | awk '{print $1}' | grep -v "^/usr/lib/" | grep -v "^/System/") + + for dep in $deps; do + local dep_name="" + + # Handle @rpath references + if [[ "$dep" == "@rpath/"* ]]; then + dep_name=$(basename "$dep") + + # Try to find this dylib in build directory + local found_path=$(find_dylib_in_build "$dep_name") + if [ -n "$found_path" ]; then + # Bundle the dylib + bundle_dylib "$found_path" + fi + + # Always change reference to use appropriate path + if [ "$is_dylib" = true ]; then + # For dylibs in lib/, use @loader_path + install_name_tool -change "$dep" "@loader_path/$dep_name" "$binary" 2>/dev/null || true + else + # For executables in bin/, use @executable_path/../lib + install_name_tool -change "$dep" "@executable_path/../lib/$dep_name" "$binary" 2>/dev/null || true + fi + elif [[ "$dep" == "@loader_path/"* ]]; then + # Already uses @loader_path - ensure the dylib is bundled and update if needed + dep_name=$(basename "$dep") + local found_path=$(find_dylib_in_build "$dep_name") + if [ -n "$found_path" ]; then + bundle_dylib "$found_path" + fi + + # Ensure correct path for context (executable vs dylib) + if [ "$is_dylib" = false ]; then + # For executables, change to @executable_path/../lib + install_name_tool -change "$dep" "@executable_path/../lib/$dep_name" "$binary" 2>/dev/null || true + fi + # For dylibs, @loader_path is already correct + elif [[ "$dep" == "@executable_path/"* ]]; then + # Already uses @executable_path + dep_name=$(basename "$dep") + local found_path=$(find_dylib_in_build "$dep_name") + if [ -n "$found_path" ]; then + bundle_dylib "$found_path" + fi + elif [[ "$dep" == *".dylib"* ]] && [ -f "$dep" ]; then + dep_name=$(basename "$dep") + + # Bundle the dylib if not already done + bundle_dylib "$dep" + + # Change reference in binary to use appropriate path + if [ "$is_dylib" = true ]; then + # For dylibs in lib/, use @loader_path + install_name_tool -change "$dep" "@loader_path/$dep_name" "$binary" 2>/dev/null || true + else + # For executables in bin/, use @executable_path/../lib + install_name_tool -change "$dep" "@executable_path/../lib/$dep_name" "$binary" 2>/dev/null || true + fi + fi + done +} + +echo -e "${GREEN}=====================================${NC}" +echo -e "${GREEN}HD Mapping - Creating Directory Bundle${NC}" +echo -e "${GREEN}=====================================${NC}" + +# Configuration +BUNDLE_NAME="HDMapping" +BUNDLE_DIR="build/${BUNDLE_NAME}" +BUILD_BIN_DIR="build/bin" +BUILD_LIB_DIR="build/lib" + +# Check if build directory exists +if [ ! -d "$BUILD_BIN_DIR" ]; then + echo -e "${RED}Error: $BUILD_BIN_DIR not found. Please build the project first.${NC}" + exit 1 +fi + +echo -e "${YELLOW}Step 1: Creating bundle directory structure...${NC}" +rm -rf "$BUNDLE_DIR" +mkdir -p "$BUNDLE_DIR/bin" +mkdir -p "$BUNDLE_DIR/lib" +echo " ✓ Created $BUNDLE_DIR/bin/" +echo " ✓ Created $BUNDLE_DIR/lib/" + +echo -e "${YELLOW}Step 2: Copying all executables to bin/...${NC}" +BINARY_COUNT=0 +for binary in "$BUILD_BIN_DIR"/*; do + if [ -f "$binary" ] && [ -x "$binary" ]; then + binary_name=$(basename "$binary") + cp "$binary" "$BUNDLE_DIR/bin/" + chmod +x "$BUNDLE_DIR/bin/$binary_name" + echo " ✓ $binary_name" + BINARY_COUNT=$((BINARY_COUNT + 1)) + fi +done +echo " Total: $BINARY_COUNT executables copied" + +echo -e "${YELLOW}Step 3: Detecting and bundling dynamic libraries...${NC}" + +# Special case: Bundle FreeGLUT (built by the project but not auto-detected) +FREEGLUT_DYLIB=$(find build/3rdparty/freeglut -name "libglut.*.*.*.dylib" -type f 2>/dev/null | head -1) +if [ -f "$FREEGLUT_DYLIB" ]; then + bundle_dylib "$FREEGLUT_DYLIB" +fi + +# Fix dylibs for all binaries +for binary in "$BUNDLE_DIR/bin"/*; do + if [ -f "$binary" ] && [ -x "$binary" ]; then + fix_binary_dylibs "$binary" + fi +done + +# Count bundled dylibs +DYLIB_COUNT=$(ls -1 "$BUNDLE_DIR/lib" 2>/dev/null | wc -l) +echo " Total: $DYLIB_COUNT dynamic libraries bundled" + +echo -e "${YELLOW}Step 4: Fixing inter-dylib dependencies...${NC}" +# Fix references between dylibs in lib/ +for dylib in "$BUNDLE_DIR/lib"/*.dylib; do + if [ -f "$dylib" ]; then + fix_binary_dylibs "$dylib" + fi +done + +# Create symlinks for versioned libraries (e.g., libglut.3.13.0.dylib -> libglut.3.dylib) +for dylib in "$BUNDLE_DIR/lib"/*.dylib; do + if [ -f "$dylib" ]; then + dylib_name=$(basename "$dylib") + # Match pattern like libXXX.#.#.#.dylib -> create libXXX.#.dylib symlink + if [[ $dylib_name =~ ^(.+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\.dylib$ ]]; then + base="${BASH_REMATCH[1]}" + major="${BASH_REMATCH[2]}" + symlink_name="${base}.${major}.dylib" + if [ ! -e "$BUNDLE_DIR/lib/$symlink_name" ]; then + cd "$BUNDLE_DIR/lib" + ln -sf "$dylib_name" "$symlink_name" + cd - > /dev/null + echo " ✓ Created symlink: $symlink_name -> $dylib_name" + fi + fi + fi +done + +echo " ✓ Fixed inter-dylib dependencies" + +echo -e "${YELLOW}Step 5: Copying README.txt...${NC}" +# Get the directory where the script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +README_SOURCE="${SCRIPT_DIR}/bundle_README.txt" + +if [ -f "$README_SOURCE" ]; then + cp "$README_SOURCE" "$BUNDLE_DIR/README.txt" + echo " ✓ README.txt copied" +else + echo -e "${RED}Warning: bundle_README.txt not found in $SCRIPT_DIR${NC}" + echo " ⚠ README.txt not copied" +fi + +echo -e "${YELLOW}Step 6: Code signing bundle...${NC}" +# Ad-hoc signing for local use +codesign --force --deep --sign - "$BUNDLE_DIR" 2>/dev/null && echo " ✓ Bundle signed (ad-hoc)" || echo " ⚠ Signing failed (bundle may still work)" + +echo "" +echo -e "${GREEN}=====================================${NC}" +echo -e "${GREEN}Bundle created successfully!${NC}" +echo -e "${GREEN}=====================================${NC}" +echo "" +echo "Location: $BUNDLE_DIR" +echo "Executables: $BINARY_COUNT" +echo "Bundled libraries: $DYLIB_COUNT" +echo "" +echo "Structure:" +echo " $BUNDLE_DIR/" +echo " ├── bin/ ($BINARY_COUNT executables)" +echo " ├── lib/ ($DYLIB_COUNT libraries)" +echo " └── README.txt" +echo "" +echo "To test the bundle:" +echo " $BUNDLE_DIR/bin/hd_mapping_launcher_gui" +echo "" +echo "To install to Applications:" +echo " cp -r $BUNDLE_DIR /Applications/" +echo "" +echo "To add to PATH:" +echo " echo 'export PATH=\"/Applications/$BUNDLE_NAME/bin:\$PATH\"' >> ~/.zshrc" +echo "" +echo "To create a DMG for distribution:" +echo " ./macos_bundle_toolkit/create_dir_bundle_dmg.sh" +echo "" diff --git a/macos_bundle_toolkit/create_dir_bundle_dmg.sh b/macos_bundle_toolkit/create_dir_bundle_dmg.sh new file mode 100755 index 00000000..aea6a636 --- /dev/null +++ b/macos_bundle_toolkit/create_dir_bundle_dmg.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# Script to create a macOS DMG package for HD Mapping directory bundle +# This script packages the HDMapping directory (not .app bundle) into a distributable .dmg +# Prerequisites: build/HDMapping directory must already exist (created by create_dir_bundle.sh) + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=====================================${NC}" +echo -e "${BLUE}HD Mapping - DMG Creation (Directory Bundle)${NC}" +echo -e "${BLUE}=====================================${NC}" +echo "" + +# Configuration +BUNDLE_NAME="HDMapping" +BUILD_DIR="build" +DMG_NAME="${BUILD_DIR}/HDMapping.dmg" +DIR_BUNDLE="${BUILD_DIR}/${BUNDLE_NAME}" + +# Verify directory bundle exists +echo -e "${YELLOW}Verifying directory bundle...${NC}" +if [ ! -d "$DIR_BUNDLE" ]; then + echo -e "${RED}Error: Directory bundle not found at $DIR_BUNDLE${NC}" + echo -e "${YELLOW}Please run ./macos_bundle_toolkit/create_dir_bundle.sh first${NC}" + exit 1 +fi + +# Check if required subdirectories exist +if [ ! -d "$DIR_BUNDLE/bin" ] || [ ! -d "$DIR_BUNDLE/lib" ]; then + echo -e "${RED}Error: Directory bundle is incomplete (missing bin/ or lib/)${NC}" + echo -e "${YELLOW}Please run ./macos_bundle_toolkit/create_dir_bundle.sh first${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Directory bundle found at $DIR_BUNDLE${NC}" +echo -e "${GREEN}✓ Contains: $(ls $DIR_BUNDLE/bin | wc -l | xargs) executables, $(ls $DIR_BUNDLE/lib/*.dylib 2>/dev/null | wc -l | xargs) libraries${NC}" +echo "" + +# Create DMG +echo -e "${YELLOW}Creating DMG package...${NC}" + +# Remove old DMG if exists +rm -f "$DMG_NAME" + +# Check if create-dmg is available +if ! command -v create-dmg &> /dev/null; then + echo -e "${RED}Error: create-dmg not found. Install it with: brew install create-dmg${NC}" + exit 1 +fi + +# Create temporary directory for DMG source +TEMP_DMG_DIR=$(mktemp -d) +echo " Creating temporary directory: $TEMP_DMG_DIR" + +# Copy the bundle to temp directory to create proper structure +# This ensures the DMG contains a folder named "HDMapping" that can be dragged to Applications +cp -R "$DIR_BUNDLE" "$TEMP_DMG_DIR/${BUNDLE_NAME}" + +# Create DMG with directory bundle +# Note: For a directory (not .app), we don't use --volicon or --hide-extension +# Try with icon positioning first, if that fails, create without icon positioning +create-dmg \ + --volname "HDMapping" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --icon "${BUNDLE_NAME}" 200 190 \ + --app-drop-link 600 185 \ + "$DMG_NAME" \ + "$TEMP_DMG_DIR" 2>&1 || \ +create-dmg \ + --volname "HDMapping" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --app-drop-link 600 185 \ + "$DMG_NAME" \ + "$TEMP_DMG_DIR" + +# Clean up temporary directory +rm -rf "$TEMP_DMG_DIR" +echo " ✓ Cleaned up temporary directory" + +# Verify DMG was created +if [ ! -f "$DMG_NAME" ]; then + echo -e "${RED}Error: DMG file was not created${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ DMG created successfully${NC}" +echo "" + +# Show summary +echo -e "${BLUE}=====================================${NC}" +echo -e "${BLUE}DMG Package Created Successfully!${NC}" +echo -e "${BLUE}=====================================${NC}" +echo "" +echo "DMG file: $DMG_NAME" +echo "Size: $(du -h "$DMG_NAME" | awk '{print $1}')" +echo "Location: $(pwd)/$DMG_NAME" +echo "" +echo "To test the DMG:" +echo " open $DMG_NAME" +echo "" +echo "To install:" +echo " 1. Mount the DMG by double-clicking" +echo " 2. Drag ${BUNDLE_NAME} folder to Applications" +echo " 3. Run tools from /Applications/${BUNDLE_NAME}/bin/" +echo "" +echo "Example usage after installation:" +echo " /Applications/${BUNDLE_NAME}/bin/hd_mapping_launcher_gui" +echo ""