How to Generate A Fat Binary For Mac
December 18, 2024
So at work, we needed to ship a binary for Mac. With Go, it is super easy to build a binary for any OS and Architecture. However, having separate binaries would mean additional logic to detect the target and then deliver them. Wouldn’t it be nice to ship just one binary which work on all Macs (Intel and Apple Silicon based) We call this a Fat Binary.
Here is how you can do it:
Consider this short Go program:
Save this as main.go
package main
import "fmt"
func main() {
fmt.Println("Fat Binary")
}
Now, you can build a binary for both the architecture like this:
GOOS=darwin GOARCH=arm64 go build -o mac_arm64 main.go
GOOS=darwin GOARCH=amd64 go build -o mac_amd64 main.go
When you run this, you will see two binaries your folder:
ls
-rwxr-xr-x@ 1 vinitkumar staff 2.1M Dec 18 18:00 mac_amd64
-rwxr-xr-x@ 1 vinitkumar staff 2.1M Dec 18 18:00 mac_arm64
Alright, this looks decent, now how do we stitch them together to make a big fat binary that works on all Macs?
Enter Lipo. This utility can take two binaries and then spit out a far binary that works on all CPU architectures. On Mac, with command line tools installed it just available as lipo
So doing which lipo
gives /usr/bin/lipo
. On Ubuntu however, it comes bundles with llvm.
So you need to do this:
sudo apt install llvm
# and then it is present as `llvm-lipo-14` depending on which llvm it is, for me it is llvm-14
So `/usr/bin/llvm-lipo-14'
Now, as we have located Lipo, now, we will use it to generate the fat binary.
# on Ubuntu
llvm-lipo-14 -create -output "mac_universal" "mac_arm64" "mac_amd64"
# on Mac
lipo -create -output "mac_universal" "main_arm64" "main_amd64"
Once created, we can verify if the binary is proper using the file util.
file mac_universal
mac_universal: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
mac_universal (for architecture x86_64): Mach-O 64-bit executable x86_64
mac_universal (for architecture arm64): Mach-O 64-bit executable arm64
You can execute this on either of the CPUs and it would just work.
Bonus code
Here is code to generate the Ascii diagram above. You need to have Pillow installed though.
from PIL import Image, ImageDraw, ImageFont
import os
# Create a new image with white background
width = 800
height = 600
image = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(image)
# Try to load a font (you may need to adjust the path)
try:
font = ImageFont.truetype("Arial.ttf", 32)
except:
font = ImageFont.load_default()
# Draw the outer rectangle
draw.rectangle([(50, 50), (width-50, height-50)], outline='black', width=2)
# Draw the vertical divider
draw.line([(width/2, 100), (width/2, 400)], fill='black', width=2)
# Draw the horizontal dividers
draw.line([(50, 250), (width-50, 250)], fill='black', width=2)
draw.line([(50, 400), (width-50, 400)], fill='black', width=2)
# Add text
draw.text((width/4, 150), "x86_64\nCode", font=font, fill='black', anchor="mm")
draw.text((3*width/4, 150), "ARM64\nCode", font=font, fill='black', anchor="mm")
draw.text((width/4, 325), "x86_64\nResources", font=font, fill='black', anchor="mm")
draw.text((3*width/4, 325), "ARM64\nResources", font=font, fill='black', anchor="mm")
draw.text((width/2, 450), "Shared Resources", font=font, fill='black', anchor="mm")
draw.text((width/2, 75), "Fat Binary", font=font, fill='black', anchor="mm")
# Save the image
image.save('fat_binary_diagram.png')
I’m a Principal Engineer and Django CMS Fellow passionate about solving meaningful problems and pushing tech boundaries. I love reading, listening/playing music, appreciating/making art, and enjoying a good cup of coffee. I hope you enjoy reading my essays.
Here are some recommendations from my current and past colleagues. Check out mylatest resume andGithub profile.
You can connect with me on twitter at @vinitkme. or drop me an email at mail@vinitkumar.me