🤖

macOSのツールチェーンの仕組み

2020/10/25に公開

macOSにはシステムにインストールされた複数のバージョンのツールチェーンを切り替えて使うための仕組みが備わっています。しかし、そのメカニズムについて記述された文書は少なく、雰囲気で xcode-select コマンドを使っている方も多いと思います。
この記事では、macOSにおけるツールチェーンの役割と仕組みについて紹介します。

ツールチェーンとは

一般的にツールチェーンとはコマンドやライブラリ、ヘッダなどをひと纏めにしたツール群のことを指すことが多いです。

例えばC言語のソースコードから実行可能なバイナリへビルドするためには、

  1. clanggccなどのコンパイラでオブジェクトファイルへ変換
  2. ld64lldなどのリンカでオブジェクトファイルとlibcを実行可能バイナリとしてリンク

という操作が必要になります。
ここで登場した、コンパイラやリンカ、標準ライブラリなどのツール郡はバラバラに配布することもできますが、ひと纏めになっている方がツール間のバージョンの固定もできて都合が良いですね。
このようにそれぞれが連携しながら動くようなツール群を一つのパッケージとしたものがツールチェーンです。

ツールチェーンの例

具体的なmacOS向けのツールチェーンには以下のようなものがあります。
他にもLLVMなどもツールチェーンとして考えることができますが、今回は後述するmacOSのxcrunと連携可能なツールチェーンのみを取り上げています。

名前 インストール場所
Xcode内蔵ツールチェーン /Applications/Xcode.app/Contents/Developer/Toolchains
OSS版 Swift /Library/Developer/Toolchains
Swift for Tensorflow /Library/Developer/Toolchains
SwiftWasm /Library/Developer/Toolchains

これらのツールチェーンは全てxctoolchainというバンドル形式で配布されており、以下のようなディレクトリ構造となっています。

├── ToolchainInfo.plist or Info.plist
└── usr
    ├── bin # コマンド
    │   ├── clang
    │   ├── swift
    │   └── ...
    ├── include # ヘッダ
    │   └── clang/c++/v1
    │       ├── stdlib.h
    │       └── ...
    ├── lib # ライブラリ
    │   ├── arc/libarclite_macosx.a
    │   └── ...
    └── ...

macOSとの連携

さて、上に挙げたようなツールチェーンに内包されているコマンド達は、/usr/bin/にインストールされている同名のコマンドから動的にツールチェーンを切り替えながら実行することができます。

(この仕組みについて詳しく言及されている文書を今の所見つけることができていないため、ここからはxcrunxcode-selectmanページの断片的な記述と、実際の挙動をベースに紹介します。)

具体的には現在アクティブになっているDeveloper Directoryと、TOOLCHAINSという環境変数によって/usr/bin/配下の特定のコマンドで実行されるバイナリが切り替わります。
xcode-selectのmanページ最下部に動的に切り替わるコマンドの一覧がありますが、ここに無いいくつかのコマンドも同様の挙動をすることがあります。

Developer Directory

Developer Directoryとは普段我々が xcode-select コマンドで切り替えているmacOS内部のパラメータです。Developer Directoryは普通Xcodeアプリ内部の/Applications/Xcode.app/Contents/Developerまたは/Library/Developer/CommandLineToolsを指しています。 DEVELOPER_DIR環境変数で一時的に上書きすることが可能です。

xcode-select --print-pathコマンドで現在の値を確認でき、xcode-select --switchコマンドでパスを変更できます。

$ xcode-select --print-path
/Applications/Xcode-12.2-beta3.app/Contents/Developer

$ DEVELOPER_DIR=/Library/Developer/CommandLineTools xcode-select --print-path
/Library/Developer/CommandLineTools

$ sudo xcode-select --switch /Applications/Xcode-12.2-beta1.app/Contents/Developer

$ xcode-select --print-path
/Applications/Xcode-12.2-beta1.app/Contents/Developer

後述するTOOLCHAINS環境変数がセットされていない場合、/usr/bin/配下のコマンドを起動すると、Developer Directoryをベースにコマンドが探索され、同名のコマンドが見つかるとそれが実行されます。

TOOLCHAINS環境変数

TOOLCHAINS環境変数にはxctoolchainバンドルのBundle Identifierまたはツールチェーンのエイリアスを指定することができます。Bundle Identifierやエイリアスはxctoolchain中のInfo.plistファイルに記述されています。

$ cat /Library/Developer/Toolchains/swift-tensorflow-RELEASE-0.9.xctoolchain/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Aliases</key>
	<array>
		<string>Swift for TensorFlow</string>
	</array>
	<key>CFBundleIdentifier</key>
	<string>com.google.swift.20200507</string>
	<key>CompatibilityVersion</key>
	<integer>2</integer>
	<key>CompatibilityVersionDisplayString</key>
	<string>Xcode 8.0</string>
	<key>CreatedDate</key>
	<date>2020-05-08T05:57:25Z</date>
	<key>DisplayName</key>
	<string>Swift for TensorFlow 0.9 Release 2020-05-07</string>
	<key>OverrideBuildSettings</key>
	<dict>
		<key>ENABLE_BITCODE</key>
		<string>NO</string>
		<key>SWIFT_DEVELOPMENT_TOOLCHAIN</key>
		<string>YES</string>
		<key>SWIFT_DISABLE_REQUIRED_ARCLITE</key>
		<string>YES</string>
		<key>SWIFT_LINK_OBJC_RUNTIME</key>
		<string>YES</string>
		<key>SWIFT_USE_DEVELOPMENT_TOOLCHAIN_RUNTIME</key>
		<string>YES</string>
	</dict>
	<key>ReportProblemURL</key>
	<string>https://bugs.swift.org/</string>
	<key>ShortDisplayName</key>
	<string>Swift for TensorFlow 0.9 Release</string>
	<key>Version</key>
	<string>5.0.20200507</string>
</dict>
</plist>

TOOLCHAINS環境変数が設定されている場合、/usr/bin/配下のコマンドを起動すると、/Library/Developer/Toolchains配下に配置されているxctoolchainの中にBundle Identifierかエイリアスが一致するものがあれば、その中のusr/bin/ディレクトリの同名のコマンドを実行します。xctoolchainが見つからない場合、Developer Directoryをベースとした探索にフォールバックします。

切り替えの優先度

以上のルールをまとめたフローの図です。

xcrun --findコマンドによって実際にどのコマンドが解決されたか調べることができます。

$ xcrun --find swift
/Applications/Xcode-12.2-beta3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift

$ DEVELOPER_DIR=/Library/Developer/CommandLineTools xcrun --find swift
/Library/Developer/CommandLineTools/usr/bin/swift

$ TOOLCHAINS=com.google.swift.20200507 xcrun --find swift
/Library/Developer/Toolchains/swift-tensorflow-RELEASE-0.9.xctoolchain/usr/bin/swift

具体例として、Swift for TensorFlow向けのプロジェクトをビルドしたい場合、以下のように環境変数を設定しながらswiftコマンドを実行します。

$ TOOLCHAINS=com.google.swift.20200507 /usr/bin/swift build

動的にコマンドが切り替わる仕組み

/usr/bin/のコマンドは具体的なバイナリへのシンボリックリンクになっているわけではなく、上のルールに沿ってコマンドを解決し引数をプロキシして実行するプログラムとなっています。

実際にディスアセンブルすると、/usr/lib/libxcselect.dylib というライブラリの_xcselect_invoke_xcrun関数を呼ぶだけの薄いラッパーであることがわかります。

$ objdump --macho --disassemble /usr/bin/swift
/usr/bin/swift:
(__TEXT,__text) section
_main:
100000f77:	55	pushq	%rbp
100000f78:	48 89 e5	movq	%rsp, %rbp
100000f7b:	8d 47 ff	leal	-1(%rdi), %eax
100000f7e:	48 8d 56 08	leaq	8(%rsi), %rdx
100000f82:	48 8d 3d 29 00 00 00	leaq	41(%rip), %rdi ## literal pool for: "swift"
100000f89:	89 c6	movl	%eax, %esi
100000f8b:	31 c9	xorl	%ecx, %ecx
100000f8d:	e8 00 00 00 00	callq	0x100000f92 ## symbol stub for: _xcselect_invoke_xcrun

また、殆どの場合問題になることはないと思いますが、コマンド解決の結果はTOOLCHAINS環境変数とDeveloper Directoryのペアをキーとしてキャッシュされることに注意してください。 xcrun --no-cache コマンドでキャッシュをリフレッシュすることができます。

このように実行されるコマンドを動的に切り替えるためのプログラムをShimと言います。rbenvなどの複数のバージョンのツールを切り替えるためのツールでよく用いられる言葉です。

Shim (computing) - Wikipedia

宣伝

これで自由自在にツールチェーンの切り替えができるようになったと思うので、SwiftWasmのツールチェーンで実際に試してみてください😁

参照

Discussion