Universal Binary
macOSはARMやx86_64や32bit i386やpowerpc用のバイナリを一つのバイナリにまとめて複数のアーキテクチャに対応できるUniversal Binaryに対応しています。標準の開発ツールのXCodeのGUIでもUniversal Binaryのビルドが可能ですが、既存のオープンソースのコードを利用する場合などコマンドラインでUniversal Binaryをビルドする方が便利な場面もあります。M1 Macのコマンドラインでの手順をまとめました。
作業環境
~/Downloads/で作業します。
$ cd ~/Downloads/
未インストールの場合はCommand line tools for Xcodeをインストールしてください。
$ xcode-select --install
lipo
lipoコマンドで複数のバイナリを1つのバイナリにまとめることができます。ARMバイナリとx86_64バイナリをそれぞれ独立してビルドし、lipoコマンドでARMバイナリとx86_64バイナリを一つにまとめることでARMとx86_64両対応のUniversal Binaryが作成できます。
次の内容でtest.cを作成します。現在の実行環境を表示するarchコマンドを実行するプログラムです。
#include <stdlib.h>
int main()
{
system("arch");
return 0;
}
-archオプションを使用してARMバイナリのtest.armとx86_64バイナリのtest.x86_64をビルドします。fileコマンドでどちらのバイナリか確認して実行します。
$ clang -o test.arm -arch arm64 test.c
$ clang -o test.x86_64 -arch x86_64 test.c
$ file test.*
test.arm: Mach-O 64-bit executable arm64
test.c: c program text, ASCII text
test.x86_64: Mach-O 64-bit executable x86_64
$ ./test.arm
arm64
$ ./test.x86_64
i386
lipoコマンドで1つのバイナリであるtestにまとめます。
$ lipo -create test.arm test.x86_64 -output test
fileコマンドで確認しそのままarmで実行とarch -x86_64でx86_64として実行の両方を試します。
$ file test
test: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
test (for architecture x86_64): Mach-O 64-bit executable x86_64
test (for architecture arm64): Mach-O 64-bit executable arm64
$ ./test
arm64
$ arch -x86_64 ./test
i386
clangで直接生成
次に、clangのオプションを利用して直接Universal Binaryを作成します。上記のtest.cをそのまま使います。
普通にビルドした場合
ARMバイナリが生成されるのでARMとしては実行できますがx86_64としては実行できません。
$ clang -o test test.c
$ file test
test: Mach-O 64-bit executable arm64
$ ./test
arm64
$ arch -arm64 ./test
arm64
$ arch -x86_64 ./test
arch: posix_spawnp: ./test: Bad CPU type in executable
同様にx86_64としてビルドした場合x86_64として実行できますがARMとしては実行できません。
$ clang -arch x86_64 -o test test.c
$ file test
test: Mach-O 64-bit executable x86_64
$ ./test
i386
$ arch -x86_64 ./test
i386
$ arch -arm64 ./test
arch: posix_spawnp: ./test: Bad CPU type in executable
Universal Binaryとしてビルドした場合
clangに-arch x86_64 -arch arm64をオプションを指定してUniversal Binaryとしてビルドします。armとしてもx86_64としても実行できるようになります。
$ clang -arch x86_64 -arch arm64 -o test test.c
$ file test
test: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
test (for architecture x86_64): Mach-O 64-bit executable x86_64
test (for architecture arm64): Mach-O 64-bit executable arm64
$ ./test
arm64
$ arch -arm64 ./test
arm64
$ arch -x86_64 ./test
i386
Rosetta2環境でビルドした場合
Rosetta2環境でビルドすると標準ではx86_64バイナリが生成されます。archコマンドでx86_64環境のzshに移って確認します。
$ arch -x86_64 /bin/zsh
$ clang -o test test.c
$ file test
test: Mach-O 64-bit executable x86_64
$ ./test
i386
なおRosetta2環境下からARMバイナリを実行しARMバイナリがarchを呼ぶ場合はx86_64としてarchコマンドを実行するようです。
$ clang -arch arm64 -o test test.c
$ file test
test: Mach-O 64-bit executable arm64
$ ./test
i386
Rosetta2環境から抜けます。
$ exit
autotoolsの場合
autotoolsはconfigureスクリプトを使って様々な環境に適合できるよう調整したりビルドオプションを指定してビルドできるようにするための開発ツールです。configureスクリプトを使ってmakeコマンド用のMakefileを作成します。GNUをはじめ多くのオープンソースプログラムで使われています。
準備としてbrewからautoconf, automake, libtoolをインストールします。
$ brew install autoconf automake libtool
brewが未インストールの場合は先にbrewをインストールして上のコマンドを実行してください。
$ arch -arm64e /bin/bash -c "$(curl -fsSL $ https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
自作のautotools対応プログラムの通常ビルド
最小限のautotools対応プログラムを作ってUniversal Binaryとしてビルドします。特別な対応は不要でconfigureスクリプトにCFLAGSやLDFLAGSを指定するだけでUniversal Binaryとしてのビルドが可能です。
autotools-test以下にMakefile.am, src/test.c, src/Makefile.amを作成します。
$ mkdir autotools-test
$ cd autotools-test
$ mkdir src
Makefile.am
SUBDIRS=src
src/Makefile.am
bin_PROGRAMS=test
test_SOURCES=test.c
src/test.cは上記をコピーしてください。
$ cp ../test.c src/
autoscanを実行しconfigure.scanをconfigure.acに変更し、中の
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
を
AC_INIT([test], [1.0], [BUG-REPORT-ADDRESS])
AM_INIT_AUTOMAKE
に書き換えます。
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
AC_INIT([test], [1.0], [BUG-REPORT-ADDRESS])
AM_INIT_AUTOMAKE
AC_CONFIG_SRCDIR([src/test.c])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
AC_CHECK_HEADERS([stdlib.h])
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_CONFIG_FILES([Makefile
src/Makefile])
AC_OUTPUT
あとは必要なコマンドを実行してconfigureスクリプトを作成します。
$ aclocal
$ autoheader
$ touch NEWS README AUTHORS ChangeLog
$ automake -a -c
$ autoconf
configureしてmakeすればARMバイナリが生成され問題なく実行でます。
./configure
make
$ file src/test
src/test: Mach-O 64-bit executable arm64
$ src/test
arm64
クリーンしておきます。
$ make clean
自作のautotools対応プログラムのUniversal Binaryビルド
CFLAGSとLDFLAGSにそれぞれ"-arch x86_64 -arch arm64"を指定することでARMとx86_64両対応でビルドされます。envコマンドで環境変数に値を与えるかexportで環境変数を設定してください。
$ env CFLAGS="-arch x86_64 -arch arm64" LDFLAGS="-arch x86_64 -arch arm64" ./configure
または
$ export CFLAGS="-arch x86_64 -arch arm64"
$ export LDFLAGS="-arch x86_64 -arch arm64"
$ ./configure
makeしてfileでUniversal Binaryになっているのを確認して実行します。
$ make
$ src/test
src/test: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
src/test (for architecture x86_64): Mach-O 64-bit executable x86_64
src/test (for architecture arm64): Mach-O 64-bit executable arm64
$ src/test
arm64
$ arch -x86_64 src/test
i386
~/Downloads/に戻ります。
$ cd ..
libpng
autotoolsを使用するオープンソースのライブラリの例としてlibpngをUniversal Binaryでビルドして、サンプルプログラムをリンクして利用してみます。
http://www.libpng.org/pub/png/libpng.html
よりlibpng-1.6.37.tar.xzをダウンロードして展開します。
$ tar xvfJ libpng-1.6.37.tar.xz
libpngのARMバイナリビルド
まず普通にビルドして/usr/local/opt/libpng-arm64以下にインストールします。
$ cd libpng-1.6.37
./configure --prefix=/usr/local/opt/libpng-arm64
make
make install
cd ..
ARMバイナリとしてビルドされます。
$ file /usr/local/opt/libpng-arm64/lib/libpng.dylib
/usr/local/opt/libpng-arm64/lib/libpng.dylib: Mach-O 64-bit dynamically linked shared library arm64
http://zarb.org/~gc/html/libpng.html
にあるコードをpngtest.cとして作成しpngtestとしてビルドします。pngファイルの画素ごとに赤を0緑を青の値にしてpngファイルに書き込むプログラムです。入力画像は適当な画像を用意してtest1.pngとしてください。出力ファイルをtest2.pngとします。
ARMバイナリとしてビルドしARMライブラリのlibpngをリンクしています。
clang -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-arm64/include -L/usr/local/opt/libpng-arm64/lib
file pngtest
pngtest: Mach-O 64-bit executable arm64
./pngtest test1.png test2.png
x86_64バイナリはうまくリンクできずエラーになります。libpngがARMバイナリだからです。
$ clang -arch x86_64 -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-arm64/include -L/usr/local/opt/libpng-arm64/lib
(略)
Undefined symbols for architecture x86_64:
"_png_create_info_struct", referenced from:
(略)
libpngのx86_64バイナリビルド
次にlibpngをx86_64バイナリとしてビルドしましょう。/usr/local/opt/libpng-x86_64にインストールします。CFLAGSとLDFLAGSに-arch x86_64を指定することでx86_64バイナリとしてビルドできます。
$ cd libpng-1.6.37
$ make clean
$ env CFLAGS="-arch x86_64" LDFLAGS="-arch x86_64" ./configure --prefix=/usr/local/opt/libpng-x86_64
$ make
$ make install
$ cd ..
% file /usr/local/opt/libpng-x86_64/lib/libpng.dylib
/usr/local/opt/libpng-x86_64/lib/libpng.dylib: Mach-O 64-bit dynamically linked shared library x86_64
pngtest.cをx86_64バイナリとしてビルドしてx86_64バイナリのlibpngとリンクします。
正常に実行できます。
$ clang -arch x86_64 -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-x86_64/include -L/usr/local/opt/libpng-x86_64/lib
$ file pngtest
pngtest: Mach-O 64-bit executable x86_64
$./pngtest test1.png test2.png
libpngのUniversal Binaryビルド
libpngをUniversal Binaryとしてビルドします。CFLAGSとLDFLAGSに-arch x86_64 -arch arm64を指定してARMとx86_64両対応でビルドします。
$ cd libpng-1.6.37
$ make clean
$ env CFLAGS="-arch x86_64 -arch arm64" LDFLAGS="-arch x86_64 -arch arm64" ./configure --prefix=/usr/local/opt/libpng-universal
$ make
$ make install
$ cd ..
ARMとx86_64対応Universal Binaryとしてビルドされています。
$ file /usr/local/opt/libpng-universal/lib/libpng.dylib
/usr/local/opt/libpng-universal/lib/libpng.dylib: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64]
/usr/local/opt/libpng-universal/lib/libpng.dylib (for architecture x86_64): Mach-O 64-bit dynamically linked shared library x86_64
/usr/local/opt/libpng-universal/lib/libpng.dylib (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64
まず普通にpngtest.cをビルドしてリンクしてみます。
$ clang -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-universal/include -L/usr/local/opt/libpng-universal/lib
ARMバイナリとしてリンクでき正常に実行できます。
$ file pngtest
pngtest: Mach-O 64-bit executable arm64
$ ./pngtest test1.png test2.png
clangに-arch x86_64を与えてx86_64としてpngtest.cをビルドしてリンクするとx86_64バイナリとしてリンクでき正常に実行できます。
$ clang -arch x86_64 -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-universal/include -L/usr/local/opt/libpng-universal/lib
$ file pngtest
pngtest: Mach-O 64-bit executable x86_64
$ ./pngtest test1.png test2.png
さらにclangに-arch x86_64 -arch arm64を与えればUniversal Binaryとしてpngtest.cをビルドしてリンクでき実行できます。
$ clang -arch x86_64 -arch arm64 -o pngtest pngtest.c -lpng -I/usr/local/opt/libpng-universal/include -L/usr/local/opt/libpng-universal/lib
$ file pngtest
pngtest: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
pngtest (for architecture x86_64): Mach-O 64-bit executable x86_64
pngtest (for architecture arm64): Mach-O 64-bit executable arm64
$ ./pngtest test1.png test2.png
CMakeの場合
CMakeは様々な環境でビルドできるようにするための開発ツールです。UnixのMakefile、macOSのXcode、WindowsのVisual Studioなどでビルドに必要なファイルを自動生成できます。
自作のCMake対応プログラムの通常ビルド
test.cをCMakeに対応させます。
cmaketest以下にtest.cとCMakeLists.txtを作成します。
$ mkdir cmaketest
$ cd cmaketest
test.cは上記をコピーしてください。
$ cp ../test.c .
CMakeLists.txtは
cmake_minimum_required(VERSION 2.8)
project(test_cmake C)
add_executable(test test.c)
とします。
build以下でcmakeでMakefileを作成してビルドします。
$ mkdir build
$ cd build
$ cmake ..
$ make
$ file test
test: Mach-O 64-bit executable arm64
$ ./test
arm64
$ cd ..
自作のCMake対応プログラムのx86_64ビルド
CMAKE_OSX_ARCHITECTURESにx86_64を指定するとclangに-arch x86_64を指定するのと同じ意味となりx86_64バイナリをビルドできます。build-x86_64以下でビルドします。
$ mkdir build-x86_64
$ cd build-x86_64
$ env CMAKE_OSX_ARCHITECTURES=x86_64 cmake ..
$ make
$ file test
test: Mach-O 64-bit executable x86_64
$ ./test
i386
$ cd ..
自作のCMake対応プログラムのUniversal Binaryビルド
CMAKE_OSX_ARCHITECTURESにx86_64;arm64を指定するとclangに-arch x86_64 -arch arm64と同じ意味になりUniversal Binaryとしてビルドできます。build-universal以下でビルドします。
$ mkdir build-universal
$ cd build-universal
$ env CMAKE_OSX_ARCHITECTURES="x86_64;arm64" cmake ..
$ file test
test: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
test (for architecture x86_64): Mach-O 64-bit executable x86_64
test (for architecture arm64): Mach-O 64-bit executable arm64
ARMとしてもx86_64としても実行できます。
$ ./test
arm64
$ arch -x86_64 ./test
i386
上に戻ります。
cd ..
i386とx86_64の場合
32bit i386とのUniversal Binaryも同様に作れそうなのですがmacOS Catalina以降は32bitバイナリ非対応なのでそのままではビルドできません。
$ clang -arch i386 -o test test.c
ld: warning: The i386 architecture is deprecated for macOS (remove from the Xcode build setting: ARCHS)
ld: warning: ignoring file /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd, missing required architecture i386 in file /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd (3 slices)
Undefined symbols for architecture i386:
"_system$UNIX2003", referenced from:
_main in test-ea1c56.o
ld: symbol(s) not found for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)
下記のサイトから32bit対応macOS用SDK(MacOSX10.13.sdk.tar.xz)をダウンロードして/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs以下にインストールしてください(10.14でも良さそうですがi386用SDKが入っていない?)。
$ tar xvfJ MacOSX10.13.sdk.tar.xz
$ sudo mv MacOSX10.13.sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs
MacOSX10.13.sdkを利用したi386とx86_64のUniversal Binaryビルド
clangに-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdkを指定することでMacOSX10.13.sdkを使用してビルドできます。
$ clang -arch i386 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk -o test test.c
ld: warning: The i386 architecture is deprecated for macOS (remove from the Xcode build setting: ARCHS)
$ file test
test: Mach-O executable i386
ただし、M1 Macは32bit i386バイナリの実行ができないのでエラーになります。
$ ./test
zsh: bad CPU type in executable: ./test
32bit i386と64bit x86_64のUniversal Binaryのtest-i386_x86_64としてビルドすればx86_64バイナリとしてM1 Macでも実行できます。
$ clang -arch i386 -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk -o test-i386_x86_64 test.c
ld: warning: The i386 architecture is deprecated for macOS (remove from the Xcode build setting: ARCHS)
$ file test-i386_x86_64
test-i386_x86_64: Mach-O universal binary with 2 architectures: [i386:Mach-O executable i386] [x86_64:Mach-O 64-bit executable x86_64]
test-i386_x86_64 (for architecture i386): Mach-O executable i386
test-i386_x86_64 (for architecture x86_64): Mach-O 64-bit executable x86_64
$ ./test-i386_x86_64
i386
逆にMacOSX10.13.sdkはARM非対応なので-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdkを指定してARMバイナリを作ろうとするとエラーになります。
$ clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk -o test test.c
In file included from test.c:1:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/stdlib.h:63:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/_types.h:27:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/sys/_types.h:32:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/sys/cdefs.h:761:2: error:
Unsupported architecture
#error Unsupported architecture
(略)
したがって、ARMとi386とx86_64のUniversal Binaryを作るためにはARMバイナリを別途ビドしてlipoで結合する必要があります。
$ clang -arch arm64 -o test-arm test.c
$ file test-arm
test-arm: Mach-O 64-bit executable arm64
$ ./test-arm
arm64
lipoでtest-i386_x86_64とtest-armを結合させてi386、x86_64、ARM対応Universal Binaryのtestを作成します。
$ lipo -create test-arm test-i386_x86_64 -output test
$ file test
test: Mach-O universal binary with 3 architectures: [i386:Mach-O executable i386] [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
test (for architecture i386): Mach-O executable i386
test (for architecture x86_64): Mach-O 64-bit executable x86_64
test (for architecture arm64): Mach-O 64-bit executable arm64
$ ./test
arm64
$ arch -x86_64 ./test
i386
autotoolsでのi386とx86_64のUniversal Binaryのビルド
CFLAGSとLDFLAGSに-arch i386 -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/を指定すればOKです。autotools-testを再利用します。
$ cd autotools-test
$ make clean
$ export CFLAGS="-arch i386 -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/"
$ export LDFLAGS="-arch i386 -arch x86_64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/"
$ ./configure
$ make
$ file src/test
src/test: Mach-O universal binary with 2 architectures: [i386:Mach-O executable i386] [x86_64:Mach-O 64-bit executable x86_64]
src/test (for architecture i386): Mach-O executable i386
src/test (for architecture x86_64): Mach-O 64-bit executable x86_64
$ src/test
i386
$ cd ..
CMakeを利用したi386とx86_64のUniversal Binaryのビルド
https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#how-do-i-build-universal-binaries-on-mac-os-x
によるとCMAKE_OSX_SYSROOTに/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/を指定するだけで良さそうですが、CFLAGSとLDFLAGSに-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/を指定しないとダメのようです。CMAKE_OSX_ARCHITECTURESにi386;x86_64を指定してi386とx86_64のUniversal Binaryとしてビルドします。
$ cd cmaketest
$ export CMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/
$ export CFLAGS="-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/"
$ export LDFLAGS="-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/"
$ export CMAKE_OSX_ARCHITECTURES="i386;x86_64"
$ mkdir build-i386_x86_64
$ cd build-i386_x86_64
$ cmake ..
$ make
$ file test
test: Mach-O universal binary with 2 architectures: [i386:Mach-O executable i386] [x86_64:Mach-O 64-bit executable x86_64]
test (for architecture i386): Mach-O executable i386
test (for architecture x86_64): Mach-O 64-bit executable x86_64
$ ./test
i386
まとめ
M1 MacでコマンドラインからUniversal Binaryとしてビルドする様々な手順を示せたと思うので、Universal Binaryのビルドが必要なときに参考にしてみてください。
参考サイト
https://news.mynavi.jp/article/osxhack-268/
https://developer.apple.com/library/archive/technotes/tn2005/tn2137.html
https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#how-do-i-build-universal-binaries-on-mac-os-x
https://www.itmedia.co.jp/enterprise/articles/0712/27/news012_2.html
https://www.webcyou.com/?p=9630
https://zenn.dev/ress/articles/069baf1c305523dfca3d
https://qiita.com/narupo/items/f63b8e768f17ce50f398
https://kamino.hatenablog.com/entry/cmake_tutorial1