After reading the post from Peter Steinberger (a must-follow) about using Apple Silicon mac minis for their continuous integration, I was intrigued by the following code Ruby snippet:

action_class do
  def download_url
    #tricky way to load this Chef::Mixin::ShellOut utilities
    Chef::Resource::RubyBlock.send(:include, Chef::Mixin::ShellOut)
    command = 'sysctl -in sysctl.proc_translated'
    command_out = shell_out(command)
    architecture = command_out.stdout == "" ? 'amd64' : 'arm64'

    platform = ['mac_os_x', 'macos'].include?(node['platform']) ? 'darwin' : 'linux'
    "https://github.com/buildkite/agent/releases/download/v#{new_resource.version}/buildkite-agent-#{platform}-#{architecture}-#{new_resource.version}.tar.gz"
  end
end

The code snippet shows a way to find out if you are running on an Intel mac, an Apple M1 mac natively or under Rosetta 2. In my previous post about Apple Silicon versus Go, I already showed how you can compile your Go code in such a way that they support both Intel and ARM (Apple Silicon) architectures in a single binary.

Let's take it a step further and see if we can figure out the actual version we're running without having to run an external program. Since Go supports syscall, let's see how far we get with it.

We first start with getting the valid from sysctl.proc_translated:

detectapplesilicon.go

package main

import (
  "syscall"
)

func main() {

  r, err := syscall.Sysctl("sysctl.proc_translated")

}

The syscall.Sysctl call fetches that value and return a byte array and an error. On an Intel mac, sysctl.proc_translated doesn't exist (as it doesn't support Rosetta 2), so in that case, an error will be returned. Since we want check if that's the actual error and not some other unrelated error, we can add the following error check:

detectapplesilicon.go

package main

import (
  "fmt"
  "runtime"
  "syscall"
)

func main() {

  r, err := syscall.Sysctl("sysctl.proc_translated")
  if err != nil && err.Error() == "no such file or directory" {
    fmt.Println("Running on intel mac, arch:", runtime.GOARCH)
  }

}

We are checking if there is an error and that it's actually the error we except. In that case, we know we're running on an Intel mac. I added the value of runtime.GOARCH as well so that we can check which slice of the binary we are running.

If there is no error, we check the resulting byte array. If it's a zero value, we are running natively on an ARM processor, if it's a non-zero value, we're running under Rosetta:

package main

import (
  "fmt"
  "runtime"
  "syscall"
)

func main() {

  r, err := syscall.Sysctl("sysctl.proc_translated")
  if err != nil && err.Error() == "no such file or directory" {
    fmt.Println("Running on intel mac, arch:", runtime.GOARCH)
  }

  if r == "\x00\x00\x00" {
    fmt.Println("Running on apple silicon natively, arch:", runtime.GOARCH)
  }

  if r == "\x01\x00\x00" {
    fmt.Println("Running on apple silicon under Rosetta 2, arch:", runtime.GOARCH)
  }

}

So, the output you will get will depend on which platform you're running on:

On an Intel mac:

$ ./detectapplesilicon
Running on intel mac, arch: amd64

Natively on an Apple Silicon mac:

$ ./detectapplesilicon
Running on apple silicon natively, arch: arm64

Running via Rosetta 2

$ arch -arch x86_64 ./detectapplesilicon
Running on apple silicon under Rosetta 2, arch: amd64

There is actually a potential bug in the above code. If an unknown error is returned and the byte array is nil, the program might crash. A safer way to check the error is:

package main

import (
  "fmt"
  "runtime"
  "syscall"
)

func main() {

  r, err := syscall.Sysctl("sysctl.proc_translated")
  if err != nil {
    if err.Error() == "no such file or directory" {
      fmt.Println("Running on intel mac, arch:", runtime.GOARCH)
    } else {
      fmt.Println("Unknown error:", err)
    }
    return
  }

  if r == "\x00\x00\x00" {
    fmt.Println("Running on apple silicon natively, arch:", runtime.GOARCH)
  }

  if r == "\x01\x00\x00" {
    fmt.Println("Running on apple silicon under Rosetta 2, arch:", runtime.GOARCH)
  }

}

I created a sample repository on Github showing how this works… You can find it here.