From 9817aba7f392aaef98c337dcd989b8a982b74f4d Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Sun, 20 Dec 2020 20:43:03 -0800 Subject: [PATCH] Support for building universal x86 / Apple Silicon (arm64) app This adds support for building MacVim as a fat binary (aka universal app) for x86_64 / arm64 in CI. The main challenge mostly lies in configuring the scripting language default search paths for the libraries, and linking against gettext. There are two possible approaches: 1. configure/build each arch completely separately, and then use `lipo` to stitch them back together. This is pretty annoying to set up, and kind of manual to do, and requires building the same thing twice, which is not great. 2. Build once with `--with-macarchs="x86_64 arm64` flag, which is what we do here. gettext: Homebrew doesn't support fat binaries, and we also need to build a custom x86 version of gettext to support down to macOS 10.9 anyway, so we manually download the bottle for arm64 gettext bottle, and then stitch it with the x86 version to create a unified binary under /usr/local/lib. This way we can just link against it in one go. Scripting languages: Add new ifdef's to load different libs under different architecture. Modify configure to support that (instead of hacking a patch in during CI like Ruby). This means while on x86_64 it will look under /usr/local/lib for Python 3, on arm64 it will look under /opt/homebrew instead (this is the recommended path for Homebrew installs for native arm64 packages). This new path is very specific to Homebrew which is not ideal, but we could change this later and maybe make the default search path logic for scripting languages smarter. Note that since there is no arm64 in CI right now, this just builds the app, but there will be no automatic testing to make sure it actually works. This is part of #1136. --- .github/workflows/ci-macvim.yaml | 71 ++++++++++++++++++++++++++++---- src/auto/configure | 59 ++++++++++++++++++++++++++ src/configure.ac | 47 +++++++++++++++++++++ src/optiondefs.h | 30 ++++++++++++++ 4 files changed, 199 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-macvim.yaml b/.github/workflows/ci-macvim.yaml index 144bdafad9..2c5aedd547 100644 --- a/.github/workflows/ci-macvim.yaml +++ b/.github/workflows/ci-macvim.yaml @@ -18,7 +18,10 @@ env: vi_cv_dll_name_perl: /System/Library/Perl/5.18/darwin-thread-multi-2level/CORE/libperl.dylib vi_cv_dll_name_python: /System/Library/Frameworks/Python.framework/Versions/2.7/Python vi_cv_dll_name_python3: /usr/local/Frameworks/Python.framework/Versions/3.9/Python + vi_cv_dll_name_python3_arm64: /opt/homebrew/Frameworks/Python.framework/Versions/3.9/Python vi_cv_dll_name_ruby: /usr/local/opt/ruby/lib/libruby.dylib + vi_cv_dll_name_ruby_arm64: /opt/homebrew/opt/ruby/lib/libruby.dylib + vi_cv_dll_name_lua_arm64: /opt/homebrew/lib/liblua.dylib VIM_BIN: src/MacVim/build/Release/MacVim.app/Contents/MacOS/Vim MACVIM_BIN: src/MacVim/build/Release/MacVim.app/Contents/MacOS/MacVim @@ -38,18 +41,27 @@ jobs: - os: macos-10.15 xcode: 11.7 - os: macos-10.15 - publish: true - os: macos-11.0 + publish: true runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - # Set up and install gettext for localization. + # Set up, install, and cache gettext library for localization. + # # Instead of using the default binary installed by Homebrew, need to build our own because gettext is statically # linked in MacVim, and need to be built against MACOSX_DEPLOYMENT_TARGET to ensure the built binary will work on # supported macOS versions. + # + # In addition, to support building a universal MacVim, we need an arm64 version of gettext as well in order to + # create a universal gettext binary to link against (Homebrew only distributes thin binaries and therefore this + # has to be done manually). To do that, we will just pull the bottle directly from Homebrew and patch it in using + # lipo. We can't use normal brew commands to get the bottle because brew doesn't natively support cross-compiling + # and we are running CI on x86_64 Macs. We also don't need to worry about the min deployment target fix on arm64 + # because all Apple Silicon Macs have to run on macOS 11+. + - name: Set up gettext if: matrix.publish run: | @@ -71,11 +83,12 @@ jobs: brew uninstall --ignore-dependencies gettext - name: Cache gettext + id: cache-gettext if: matrix.publish uses: actions/cache@v2 with: path: /usr/local/Cellar/gettext - key: gettext-homebrew-cache-${{ runner.os }}-${{ hashFiles('gettext.rb') }} + key: gettext-homebrew-cache-patched-unified-${{ hashFiles('gettext.rb') }} - name: Install gettext if: matrix.publish @@ -85,6 +98,37 @@ jobs: brew install -s gettext.rb # This will be a no-op if gettext was cached brew link gettext # If gettext was cached, this step is necessary to relink it to /usr/local/ + - name: Create universal gettext with arm64 bottle + if: matrix.publish && steps.cache-gettext.outputs.cache-hit != 'true' + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + run: | + set -o verbose + + # Manually download and extract gettext bottle for arm64 + gettext_url=$(brew info --json gettext | ruby -rjson -e 'j = JSON.parse(STDIN.read); puts j[0]["bottle"]["stable"]["files"]["arm64_big_sur"]["url"]') + gettext_ver=$(brew info --json gettext | ruby -rjson -e 'j = JSON.parse(STDIN.read); puts j[0]["versions"]["stable"]') + + mkdir gettext_download + cd gettext_download + wget --no-verbose ${gettext_url} + tar xf gettext*.tar.gz + + # Just for diagnostics, print out the old archs. This should be a thin binary (x86_64) + lipo -info /usr/local/lib/libintl.a + lipo -info /usr/local/lib/libintl.dylib + + # Create a universal binary by patching the custom built x86_64 one with the downloaded arm64 one. + # Modify the actual binaries in /usr/local/Cellar instead of the symlinks to allow caching to work. + lipo -create -output /usr/local/Cellar/gettext/${gettext_ver}/lib/libintl.a /usr/local/Cellar/gettext/${gettext_ver}/lib/libintl.a ./gettext/${gettext_ver}/lib/libintl.a + lipo -create -output /usr/local/Cellar/gettext/${gettext_ver}/lib/libintl.dylib /usr/local/Cellar/gettext/${gettext_ver}/lib/libintl.dylib ./gettext/${gettext_ver}/lib/libintl.dylib + + # Print out the new archs and verify they are universal with 2 archs. + lipo -info /usr/local/lib/libintl.a | grep 'x86_64 arm64' + lipo -info /usr/local/lib/libintl.dylib | grep 'x86_64 arm64' + + # Set up remaining packages and tools + - name: Install packages if: matrix.publish env: @@ -101,6 +145,8 @@ jobs: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer xcode-select -p + # All set up steps are done. Build and test MacVim below. + - name: Configure run: | set -o verbose @@ -111,7 +157,6 @@ jobs: --with-tlib=ncurses --enable-cscope --enable-gui=macvim - --with-macarchs=x86_64 --with-compiledby="GitHub Actions" ) if ${{ matrix.publish == true }}; then @@ -122,6 +167,11 @@ jobs: --enable-rubyinterp=dynamic --enable-luainterp=dynamic --with-lua-prefix=/usr/local + --with-macarchs="x86_64 arm64" + ) + else + CONFOPT+=( + --with-macarchs=x86_64 ) fi echo "CONFOPT: ${CONFOPT[@]}" @@ -140,6 +190,12 @@ jobs: grep -q -- "-DDYNAMIC_PYTHON3_DLL=\\\\\"${vi_cv_dll_name_python3}\\\\\"" src/auto/config.mk grep -q -- "-DDYNAMIC_RUBY_DLL=\\\\\"${vi_cv_dll_name_ruby}\\\\\"" src/auto/config.mk + # Also search for the arm64 overrides for the default library locations, which are different from x86_64 + # because Homebrew puts them at a different place. + grep -q -- "-DDYNAMIC_PYTHON3_DLL_ARM64=\\\\\"${vi_cv_dll_name_python3_arm64}\\\\\"" src/auto/config.mk + grep -q -- "-DDYNAMIC_RUBY_DLL_ARM64=\\\\\"${vi_cv_dll_name_ruby_arm64}\\\\\"" src/auto/config.mk + grep -q -- "-DDYNAMIC_LUA_DLL_ARM64=\\\\\"${vi_cv_dll_name_lua_arm64}\\\\\"" src/auto/config.mk + - name: Show configure output run: | cat src/auto/config.mk @@ -185,12 +241,11 @@ jobs: echo 'Found external dynamic linkage!'; false fi - # Make sure we are building x86_64 only. arm64 builds don't work properly now, so we don't want to accidentally build - # it as it will get prioritized by Apple Silicon Macs. + # Make sure we are building universal x86_64 / arm64 builds and didn't accidentally create a thin app. check_arch() { local archs=($(lipo -archs "$1")) - if [[ ${archs[@]} != x86_64 ]]; then - echo "Found unexpected arch(s) in $1: ${archs[@]}"; false + if [[ ${archs[@]} != "x86_64 arm64" ]]; then + echo "Wrong arch(s) in $1: ${archs[@]}"; false fi } check_arch "${VIM_BIN}" diff --git a/src/auto/configure b/src/auto/configure index d220ffd6e4..c7c2334e11 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -5673,6 +5673,20 @@ $as_echo "yes" >&6; } LUA_LIBS="" LUA_CFLAGS="-DDYNAMIC_LUA_DLL=\\\"${vi_cv_dll_name_lua}\\\" $LUA_CFLAGS" + + # MacVim patch to hack in a different default dynamic lib path for + # arm64. We don't test that it links here so this has to be binary + # compatible with DYNAMIC_LUA_DLL + { $as_echo "$as_me:${as_lineno-$LINENO}: checking liblua${luajit}*.${ext}* (arm64)" >&5 +$as_echo_n "checking liblua${luajit}*.${ext}* (arm64)... " >&6; } + if test -n "${vi_cv_dll_name_lua_arm64}"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${vi_cv_dll_name_lua_arm64}" >&5 +$as_echo "${vi_cv_dll_name_lua_arm64}" >&6; } + LUA_CFLAGS+=" -DDYNAMIC_LUA_DLL_ARM64=\\\"${vi_cv_dll_name_lua_arm64}\\\"" + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: " >&5 +$as_echo "" >&6; } + fi fi if test "X$LUA_CFLAGS$LUA_LIBS" != "X" && \ test "x$MACOS_X" = "xyes" && test "x$vi_cv_with_luajit" != "xno" && \ @@ -7178,6 +7192,20 @@ fi PYTHON3_OBJ="objects/if_python3.o" PYTHON3_CFLAGS="$PYTHON3_CFLAGS -DDYNAMIC_PYTHON3_DLL=\\\"${vi_cv_dll_name_python3}\\\"" PYTHON3_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for arm64. + # We don't test that it links here so this has to be binary compatible with + # DYNAMIC_PYTHON3_DLL + { $as_echo "$as_me:${as_lineno-$LINENO}: checking Python3's dll name (arm64)" >&5 +$as_echo_n "checking Python3's dll name (arm64)... " >&6; } + if test -n "${vi_cv_dll_name_python3_arm64}"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${vi_cv_dll_name_python3_arm64}" >&5 +$as_echo "${vi_cv_dll_name_python3_arm64}" >&6; } + PYTHON3_CFLAGS+=" -DDYNAMIC_PYTHON3_DLL_ARM64=\\\"${vi_cv_dll_name_python3_arm64}\\\"" + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: " >&5 +$as_echo "" >&6; } + fi elif test "$python_ok" = yes && test "$enable_pythoninterp" = "dynamic"; then $as_echo "#define DYNAMIC_PYTHON 1" >>confdefs.h @@ -7224,6 +7252,20 @@ elif test "$python3_ok" = yes && test "$enable_python3interp" = "dynamic"; then PYTHON3_OBJ="objects/if_python3.o" PYTHON3_CFLAGS="$PYTHON3_CFLAGS -DDYNAMIC_PYTHON3_DLL=\\\"${vi_cv_dll_name_python3}\\\"" PYTHON3_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for arm64. + # We don't test that it links here so this has to be binary compatible with + # DYNAMIC_PYTHON3_DLL + { $as_echo "$as_me:${as_lineno-$LINENO}: checking Python3's dll name (arm64)" >&5 +$as_echo_n "checking Python3's dll name (arm64)... " >&6; } + if test -n "${vi_cv_dll_name_python3_arm64}"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${vi_cv_dll_name_python3_arm64}" >&5 +$as_echo "${vi_cv_dll_name_python3_arm64}" >&6; } + PYTHON3_CFLAGS+=" -DDYNAMIC_PYTHON3_DLL_ARM64=\\\"${vi_cv_dll_name_python3_arm64}\\\"" + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: " >&5 +$as_echo "" >&6; } + fi elif test "$python3_ok" = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking if -fPIE can be added for Python3" >&5 $as_echo_n "checking if -fPIE can be added for Python3... " >&6; } @@ -7763,6 +7805,23 @@ $as_echo "$rubyhdrdir" >&6; } RUBY_CFLAGS="-DDYNAMIC_RUBY_DLL=\\\"$libruby_soname\\\" $RUBY_CFLAGS" RUBY_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for + # arm64. We don't test that it links here so this has to be binary + # compatible with DYNAMIC_RUBY_DLL + # Note: Apple does ship with a default Ruby lib, but it's usually older + # than Homebrew, and since on x86_64 we use the Homebrew version, we + # should use that as well for Apple Silicon. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking ${libruby_soname} (arm64)" >&5 +$as_echo_n "checking ${libruby_soname} (arm64)... " >&6; } + if test -n "${vi_cv_dll_name_ruby_arm64}"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: ${vi_cv_dll_name_ruby_arm64}" >&5 +$as_echo "${vi_cv_dll_name_ruby_arm64}" >&6; } + RUBY_CFLAGS+=" -DDYNAMIC_RUBY_DLL_ARM64=\\\"${vi_cv_dll_name_ruby_arm64}\\\"" + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: " >&5 +$as_echo "" >&6; } + fi fi else { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found; disabling Ruby" >&5 diff --git a/src/configure.ac b/src/configure.ac index 54655da169..b4b81df3b6 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -777,6 +777,17 @@ if test "$enable_luainterp" = "yes" -o "$enable_luainterp" = "dynamic"; then AC_DEFINE(DYNAMIC_LUA) LUA_LIBS="" LUA_CFLAGS="-DDYNAMIC_LUA_DLL=\\\"${vi_cv_dll_name_lua}\\\" $LUA_CFLAGS" + + # MacVim patch to hack in a different default dynamic lib path for + # arm64. We don't test that it links here so this has to be binary + # compatible with DYNAMIC_LUA_DLL + AC_MSG_CHECKING([liblua${luajit}*.${ext}* (arm64)]) + if test -n "${vi_cv_dll_name_lua_arm64}"; then + AC_MSG_RESULT([${vi_cv_dll_name_lua_arm64}]) + LUA_CFLAGS+=" -DDYNAMIC_LUA_DLL_ARM64=\\\"${vi_cv_dll_name_lua_arm64}\\\"" + else + AC_MSG_RESULT([]) + fi fi if test "X$LUA_CFLAGS$LUA_LIBS" != "X" && \ test "x$MACOS_X" = "xyes" && test "x$vi_cv_with_luajit" != "xno" && \ @@ -1799,6 +1810,17 @@ if test "$python_ok" = yes && test "$python3_ok" = yes; then PYTHON3_OBJ="objects/if_python3.o" PYTHON3_CFLAGS="$PYTHON3_CFLAGS -DDYNAMIC_PYTHON3_DLL=\\\"${vi_cv_dll_name_python3}\\\"" PYTHON3_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for arm64. + # We don't test that it links here so this has to be binary compatible with + # DYNAMIC_PYTHON3_DLL + AC_MSG_CHECKING([Python3's dll name (arm64)]) + if test -n "${vi_cv_dll_name_python3_arm64}"; then + AC_MSG_RESULT([${vi_cv_dll_name_python3_arm64}]) + PYTHON3_CFLAGS+=" -DDYNAMIC_PYTHON3_DLL_ARM64=\\\"${vi_cv_dll_name_python3_arm64}\\\"" + else + AC_MSG_RESULT([]) + fi elif test "$python_ok" = yes && test "$enable_pythoninterp" = "dynamic"; then AC_DEFINE(DYNAMIC_PYTHON) PYTHON_SRC="if_python.c" @@ -1827,6 +1849,17 @@ elif test "$python3_ok" = yes && test "$enable_python3interp" = "dynamic"; then PYTHON3_OBJ="objects/if_python3.o" PYTHON3_CFLAGS="$PYTHON3_CFLAGS -DDYNAMIC_PYTHON3_DLL=\\\"${vi_cv_dll_name_python3}\\\"" PYTHON3_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for arm64. + # We don't test that it links here so this has to be binary compatible with + # DYNAMIC_PYTHON3_DLL + AC_MSG_CHECKING([Python3's dll name (arm64)]) + if test -n "${vi_cv_dll_name_python3_arm64}"; then + AC_MSG_RESULT([${vi_cv_dll_name_python3_arm64}]) + PYTHON3_CFLAGS+=" -DDYNAMIC_PYTHON3_DLL_ARM64=\\\"${vi_cv_dll_name_python3_arm64}\\\"" + else + AC_MSG_RESULT([]) + fi elif test "$python3_ok" = yes; then dnl Check that adding -fPIE works. It may be needed when using a static dnl Python library. @@ -2084,6 +2117,20 @@ if test "$enable_rubyinterp" = "yes" -o "$enable_rubyinterp" = "dynamic"; then AC_DEFINE(DYNAMIC_RUBY) RUBY_CFLAGS="-DDYNAMIC_RUBY_DLL=\\\"$libruby_soname\\\" $RUBY_CFLAGS" RUBY_LIBS= + + # MacVim patch to hack in a different default dynamic lib path for + # arm64. We don't test that it links here so this has to be binary + # compatible with DYNAMIC_RUBY_DLL + # Note: Apple does ship with a default Ruby lib, but it's usually older + # than Homebrew, and since on x86_64 we use the Homebrew version, we + # should use that as well for Apple Silicon. + AC_MSG_CHECKING([${libruby_soname} (arm64)]) + if test -n "${vi_cv_dll_name_ruby_arm64}"; then + AC_MSG_RESULT([${vi_cv_dll_name_ruby_arm64}]) + RUBY_CFLAGS+=" -DDYNAMIC_RUBY_DLL_ARM64=\\\"${vi_cv_dll_name_ruby_arm64}\\\"" + else + AC_MSG_RESULT([]) + fi fi else AC_MSG_RESULT(not found; disabling Ruby) diff --git a/src/optiondefs.h b/src/optiondefs.h index f41dddfd47..5facee9264 100644 --- a/src/optiondefs.h +++ b/src/optiondefs.h @@ -309,6 +309,36 @@ struct vimoption # define DEFAULT_PYTHON_VER 0 #endif +// Support targeting different dynamic linkages for scripting languages based on +// arch on macOS. This is necessary because package managers such as Homebrew +// distributes thin binaries, and therefore the x86_64 and arm64 libraries are +// located in different places. +#ifdef MACOS_X +# if defined(DYNAMIC_PYTHON3_DLL_X86_64) && defined(__x86_64__) +# undef DYNAMIC_PYTHON3_DLL +# define DYNAMIC_PYTHON3_DLL DYNAMIC_PYTHON3_DLL_X86_64 +# elif defined(DYNAMIC_PYTHON3_DLL_ARM64) && defined(__arm64__) +# undef DYNAMIC_PYTHON3_DLL +# define DYNAMIC_PYTHON3_DLL DYNAMIC_PYTHON3_DLL_ARM64 +# endif + +# if defined(DYNAMIC_RUBY_DLL_X86_64) && defined(__x86_64__) +# undef DYNAMIC_RUBY_DLL +# define DYNAMIC_RUBY_DLL DYNAMIC_RUBY_DLL_X86_64 +# elif defined(DYNAMIC_RUBY_DLL_ARM64) && defined(__arm64__) +# undef DYNAMIC_RUBY_DLL +# define DYNAMIC_RUBY_DLL DYNAMIC_RUBY_DLL_ARM64 +# endif + +# if defined(DYNAMIC_LUA_DLL_X86_64) && defined(__x86_64__) +# undef DYNAMIC_LUA_DLL +# define DYNAMIC_LUA_DLL DYNAMIC_LUA_DLL_X86_64 +# elif defined(DYNAMIC_LUA_DLL_ARM64) && defined(__arm64__) +# undef DYNAMIC_LUA_DLL +# define DYNAMIC_LUA_DLL DYNAMIC_LUA_DLL_ARM64 +# endif +#endif + // used for 'cinkeys' and 'indentkeys' #define INDENTKEYS_DEFAULT (char_u *)"0{,0},0),0],:,0#,!^F,o,O,e"