環境
- Mac OS: sonoma 14.0.1
 - Chip: Apple Silicon M2
 - lima: 1.0.1
 
前提知識
Qemu
Quick Emulatorの略で、『コンピュータのCPU(MPU/マイクロプロセッサ)や各種の制御回路の動作をソフトウェアによって再現し、実際のオペレーティングシステム(OS)やアプリケーションソフトを導入して動作させることができる。Windows上のQEMU環境でLinuxを起動するなど、QEMUが実行されているOSとQEMU上で実行するOSは異なっていてもよい。』というやつ

limaでvmを作成する際に、qemuを使うことでIntelプロセッサをベースにしたイメージのコンテナを立てることができる。
ただし、めちゃくちゃ遅く、自分のローカル環境ではE2EテストでAPIがタイムアウトになっていたので、できれば使いたくない
Rosettta
Rosettaとは、Apple シリコン(arm)を搭載した Mac でも、Intel プロセッサ搭載 Mac 用に開発されたアプリを使えるようにするためのツール。
このRosettaをlimaのvmで利用するようにすれば、vmをarmで立てた上で、その上にintelプロセッサのイメージのコンテナを立てることができる
速度的にもQemuを使ってlimaのvmを立てるよりも早い
WSのE2Eテストをローカルで回す場合だと数倍程度早くなる
やろうとしていたこと
limaでarmアーキテクチャかつrosettaを利用するvmを立てた上で、そのvm上でx86アーキテクチャ(intel)のイメージであるSQL Serverのコンテナを立てることをやろうとしていました。
# limaのvmを立てるためのyamlファイル
...
mountType: virtiofs
## Rosettaを利用するように設定
rosetta:
  enabled: true
  binfmt: true
...
## aarch64(arm)のvmを立てるように設定
images:
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
  arch: "aarch64"
vmType: vz
arch: aarch64
# SQL Serverコンテナを立てるためのdocker-compose.yml
services:
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-CU15-GDR1-ubuntu-22.04
    container_name: mssql
    environment:
      - ACCEPT_EULA=Y
      - MSSQL_SA_PASSWORD=Password123
      - MSSQL_PID=Standard
    ports:
      - '1433:1433'
    restart: always
    user: root
    volumes:
      - db_data:/var/opt/mssql
    healthcheck:
      test: [ "CMD", "/opt/mssql-tools18/bin/sqlcmd", "-U", "sa", "-P", "Password123", "-Q", "SELECT 1", "-C" ]
      interval: 10s
      timeout: 5s
      start_period: 10s
      retries: 10
事象「VMは立てられるがSQL Serverのコンテナがうまく立ち上がらない」
具体的に発生していた事象としては、SQL Serverのコンテナをdocker compose upで立てようとした際に、ステータスがunhealthyで再起動を繰り返していました。
! mssql The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested                          0.0s
container mssql is unhealthy
またdocker logsでSQL Serverコンテナ内のログを確認すると、以下のエラーを吐き続けていました。
勘のいい人なら、「あれCPUアーキテクチャの差異をRosettaが吸収できていないのでは?」と気づくと思います。
ただ、社内の他の開発者はうまく動作していたので、自分固有の問題という感じでした。
exec /opt/mssql/bin/permissions_check.sh: exec format error
ちなみにlimaのvmを作成する際の処理では以下の警告が出ていました.
Rosettaを使うように設定する処理がうまくいかなかったという類のエラーですね.
WARN[0314] [hostagent] Unable to configure Rosetta: 
failed to create a new rosetta directory share caching option: 
unsupported build target macOS version for 14.0 
(the binary was built with __MAC_OS_X_VERSION_MAX_ALLOWED=130300; 
needs recompilation)
原因「Mac OS Venture(13)の時にbrew install したlimaを使っていたため、limaのvmを立てる際の処理でsonoma以上でないと使えない処理を振り分ける条件分岐でエラーになっていた
結論、原因はMac OS Vernture時代にbrew installしてビルドしたlimaが原因でした。
一つ一つ処理を追っていきます
limaのソースコードを追ってみる
まずlimaのソースコードでlimaのvm作成時に吐いていた警告「Unable to configure Rosetta」をgrepしてみます。
そうするとattachFolderMountsという関数ないでエラーログを吐いていることがわかります。
ここから、さらにエラーの原因となる関数createRosettaDirectoryShareConfigurationのソースコードも見てみます。
// vm_darwin.go
func attachFolderMounts(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfiguration) error {
    var mounts []vz.DirectorySharingDeviceConfiguration
    if *driver.Instance.Config.MountType == limayaml.VIRTIOFS {
        for i, mount := range driver.Instance.Config.Mounts {
            expandedPath, err := localpathutil.Expand(mount.Location)
            if err != nil {
                return err
            }
            if _, err := os.Stat(expandedPath); errors.Is(err, os.ErrNotExist) {
                err := os.MkdirAll(expandedPath, 0o750)
                if err != nil {
                    return err
                }
            }
            directory, err := vz.NewSharedDirectory(expandedPath, !*mount.Writable)
            if err != nil {
                return err
            }
            share, err := vz.NewSingleDirectoryShare(directory)
            if err != nil {
                return err
            }
            tag := fmt.Sprintf("mount%d", i)
            config, err := vz.NewVirtioFileSystemDeviceConfiguration(tag)
            if err != nil {
                return err
            }
            config.SetDirectoryShare(share)
            mounts = append(mounts, config)
        }
    }
    if *driver.Instance.Config.Rosetta.Enabled {
        logrus.Info("Setting up Rosetta share")
        // エラーを渡している関数はここ
        directorySharingDeviceConfig, err := createRosettaDirectoryShareConfiguration()
        if err != nil {
                         // ここで対象のエラーログを吐いていいる
            logrus.Warnf("Unable to configure Rosetta: %s", err)
        } else {
            mounts = append(mounts, directorySharingDeviceConfig)
        }
    }
    if len(mounts) > 0 {
        vmConfig.SetDirectorySharingDevicesVirtualMachineConfiguration(mounts)
    }
    return nil
}
そうするとlimaのvm作成時のエラーメッセージとして表示されていた詳細がさらに見つかりました。
// lima-vm/lima/pkg/vz/rosetta_directory_share_arm64.go
func createRosettaDirectoryShareConfiguration() (*vz.VirtioFileSystemDeviceConfiguration, error) {
    config, err := vz.NewVirtioFileSystemDeviceConfiguration("vz-rosetta")
    if err != nil {
        return nil, fmt.Errorf("failed to create a new virtio file system configuration: %w", err)
    }
    availability := vz.LinuxRosettaDirectoryShareAvailability()
    switch availability {
    case vz.LinuxRosettaAvailabilityNotSupported:
        return nil, errRosettaUnsupported
    case vz.LinuxRosettaAvailabilityNotInstalled:
        logrus.Info("Installing rosetta...")
        logrus.Info("Hint: try `softwareupdate --install-rosetta` if Lima gets stuck here")
        if err := vz.LinuxRosettaDirectoryShareInstallRosetta(); err != nil {
            return nil, fmt.Errorf("failed to install rosetta: %w", err)
        }
        logrus.Info("Rosetta installation complete.")
    case vz.LinuxRosettaAvailabilityInstalled:
        // nothing to do
    }
    rosettaShare, err := vz.NewLinuxRosettaDirectoryShare()
    if err != nil {
        return nil, fmt.Errorf("failed to create a new rosetta directory share: %w", err)
    }
    macOSProductVersion, err := osutil.ProductVersion()
    if err != nil {
        return nil, fmt.Errorf("failed to get macOS product version: %w", err)
    }
    if !macOSProductVersion.LessThan(*semver.New("14.0.0")) {
                // エラーログの原因となる関数
        cachingOption, err := vz.NewLinuxRosettaAbstractSocketCachingOptions("rosetta")
        if err != nil {
                        // ここでvm作成時のエラーログが吐かれている
            return nil, fmt.Errorf("failed to create a new rosetta directory share caching option: %w", err)
        }
        rosettaShare.SetOptions(cachingOption)
    }
    config.SetDirectoryShare(rosettaShare)
    return config, nil
}
vzのソースコードを追ってみる
さらにエラーメッセージの原因となる関数vz.NewLinuxRosettaAbstractSocketCachingOptions("rosetta")を追ってみます。
この関数はlimaとは別のライブラリgithub.com/Code-Hex/vzの関数でした。
コードを追ってみると「This is only supported on macOS 14 and newer」というコメントがあり、macOS14より新しくないとダメだよという記載が見つかります。
// Code-Hex/vz/shared_directory_arm64.go
// NewLinuxRosettaAbstractSocketCachingOptions creates a new LinuxRosettaAbstractSocketCachingOptions.
//
// The name of the Abstract Socket to be used to communicate with the Rosetta translation daemon.
//
// This is only supported on macOS 14 and newer, error will
// be returned on older versions.
func NewLinuxRosettaAbstractSocketCachingOptions(name string) (*LinuxRosettaAbstractSocketCachingOptions, error) {
    if err := macOSAvailable(14); err != nil {
        return nil, err
    }
    maxNameLen := maximumNameLengthVZLinuxRosettaAbstractSocketCachingOptions()
    if maxNameLen < len(name) {
        return nil, fmt.Errorf("name length exceeds maximum allowed length of %d", maxNameLen)
    }
    cs := charWithGoString(name)
    defer cs.Free()
    nserrPtr := newNSErrorAsNil()
    asco := &LinuxRosettaAbstractSocketCachingOptions{
        pointer: objc.NewPointer(
            C.newVZLinuxRosettaAbstractSocketCachingOptionsWithName(cs.CString(), &nserrPtr),
        ),
    }
    if err := newNSError(nserrPtr); err != nil {
        return nil, err
    }
    objc.SetFinalizer(asco, func(self *LinuxRosettaAbstractSocketCachingOptions) {
        objc.Release(self)
    })
    return asco, nil
}
func maximumNameLengthVZLinuxRosettaAbstractSocketCachingOptions() int {
    return int(uint32(C.maximumNameLengthVZLinuxRosettaAbstractSocketCachingOptions()))
}
ここでlimaのvm作成時の警告メッセージに戻って確認すると、以下のように「macOSのバージョンが14.0に対応しているビルド対象じゃないよ. このバイナリは__MAC_OS_X_VERSION_MAX_ALLOWED=130300でビルドされているよ. 再コンパイルが必要だよ」とあります.
unsupported build target macOS version for 14.0 
(the binary was built with __MAC_OS_X_VERSION_MAX_ALLOWED=130300; 
needs recompilation)
「__MAC_OS_X_VERSION_MAX_ALLOWED」という定数で改めてvzのソースコードをgrepしてみます。
そうすると「Code-Hex/vz/virtualization_helper.h」というヘッダーファイルに__MAC_OS_X_VERSION_MAX_ALLOWEDの記載がありました。
よく読んでみると、__MAC_OS_X_VERSION_MAX_ALLOWEDが130000以上の場合はmacOS 13 APIではdisabledされているとあります。
# Code-Hex/vz/virtualization_helper.h
#pragma once
#import <Availability.h>
#import <Foundation/Foundation.h>
NSDictionary *dumpProcessinfo();
#define RAISE_REASON_MESSAGE                                                                               \
    "This may possibly be a bug due to library handling errors.\n"                                         \
    "I would appreciate it if you could report it to https://github.com/Code-Hex/vz/issues/new/choose\n\n" \
    "Information: %@\n"
#define RAISE_UNSUPPORTED_MACOS_EXCEPTION()                   \
    do {                                                      \
        [NSException                                          \
             raise:@"UnhandledAvailabilityException"          \
            format:@RAISE_REASON_MESSAGE, dumpProcessinfo()]; \
        __builtin_unreachable();                              \
    } while (0)
// for macOS 12.3 API
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 120300
#define INCLUDE_TARGET_OSX_12_3 1
#else
#pragma message("macOS 12.3 API has been disabled")
#endif
// for macOS 13 API
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000
#define INCLUDE_TARGET_OSX_13 1
#else
#pragma message("macOS 13 API has been disabled")
#endif
// for macOS 14 API
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
#define INCLUDE_TARGET_OSX_14 1
#else
#pragma message("macOS 14 API has been disabled")
#endif
あれ私のmacOS 14なんだが、、、と思ったのですが、ここでもう一度limaのvm作成時の警告メッセージに立ち返ります。
ここで、macOS14でしか使えないAPIだよとエラーが出ているのに、実際のmacOSは14なので
「あれ私のlima、ローカルのOSをmacOS13として認識してないか、、?」ということがわかります。
unsupported build target macOS version for 14.0 
(the binary was built with __MAC_OS_X_VERSION_MAX_ALLOWED=130300; 
needs recompilation)
brew installしたときはmacOSは13だった
ここで過去を振り返ってみると、limaをbrew installしたとき、自分のmacOSは13でした。
そのため、その時点でlimaをbrew installした場合、macOS13としてビルドされたlimaをinstallしていたっぽいです。
そのため、limaを動かした際に、「お前macOS13だからこのAPI使えなくてRosettaうまく動かせないよ」という警告が出ていたということでした。
※ ここら辺の理解が怪しいので、間違っているかもですが、ざっくり言うと上記が原因
解決策: limaをinstallし直す
解決策だけ見てしまうとあっけないですが、limaをbrew uninstallした上で、再度installし直すことで、macOS14としてビルドされたlimaがインストールされるので、この問題は解決しました。
学び
ツールがうまく動作しない場合、ツールが吐いているlogをツールのソースコードでgrepしてみると色々わかることがあり調査が捗るなと思いました。
  
  
  
  
