Build and Check a Universal Mach-O with llvm-lipo-20
You will combine thin x86_64 and arm64 Mach-O files into one universal file, inspect the architectures it contains, and extract a thin file again. The examples use llvm-lipo-20 from Debian package llvm-20, version 20.1.8, installed on this machine. Allow about fifteen minutes if you already have the two input files. The commands only write files in the named output paths; none needs elevated privileges.
The route
Jump straight to the step you need, or tick off Done means at the end.
1. Check the installed tool
Start by confirming that the command on your PATH is the one you intend to use:
$ command -v llvm-lipo-20
/usr/bin/llvm-lipo-20
$ llvm-lipo-20 -version
llvm-lipo
Ubuntu LLVM version 20.1.8
Optimized build.
llvm-lipo handles Mach-O files. It can describe thin and universal files, verify architectures, create a universal file, make a thin output from a universal input, and replace one architecture slice. It does not compile source code or link an executable.
Checkpoint: confirm that your inputs are Mach-O files for architectures you actually intend to distribute. An ELF object from a normal Linux build is not a substitute.
2. Inspect the thin inputs first
Use -info with one or more input files. Put the file names before the operation options. The following names are placeholders:
$ llvm-lipo-20 /path/to/program-x86_64.o /path/to/program-arm64.o -info
Non-fat file: /path/to/program-x86_64.o is architecture: x86_64
Non-fat file: /path/to/program-arm64.o is architecture: arm64
The exact spacing and path text can vary, but each input should be reported as a non-fat file with the expected architecture. For a script or a compact check, -archs prints the architecture names separated by whitespace:
$ llvm-lipo-20 /path/to/program-x86_64.o -archs
x86_64
Do not continue if an input is reported as the wrong architecture. Rebuild it for the intended target and inspect it again. This avoids producing a valid universal container whose slices do not match their labels.
3. Create the universal file
Pass at least one input to -create and provide a separate output path with -output. Two different architectures are the useful case:
$ llvm-lipo-20 \
/path/to/program-x86_64.o \
/path/to/program-arm64.o \
-create \
-output /path/to/program-universal.o
A successful command normally prints nothing. The output path is created or replaced, so treat it as a state-changing step even though it does not require sudo. Do not point it at an input you need to preserve.
Verify the result immediately:
$ llvm-lipo-20 /path/to/program-universal.o -info
Architectures in the fat file: /path/to/program-universal.o are: x86_64 arm64
$ llvm-lipo-20 /path/to/program-universal.o -archs
x86_64 arm64
Universal Mach-O files are also called fat files in the tool's diagnostics. That wording does not mean the file contains source code or two complete application installations; it contains architecture-specific Mach-O slices.
4. Check an architecture without parsing output
-verify_arch exits with status 0 when every architecture named in the check is present, and status 1 otherwise. Put the input file before the option:
$ llvm-lipo-20 /path/to/program-universal.o -verify_arch x86_64 arm64
$ printf 'status=%s\n' "$?"
status=0
$ llvm-lipo-20 /path/to/program-universal.o -verify_arch arm64 ppc
$ printf 'status=%s\n' "$?"
status=1
Use the exit status in build or packaging automation rather than grepping the human-readable -info text. With set -e, a deliberate negative test will stop the shell, so capture or branch on the status when failure is expected.
5. Extract a thin slice safely
When a downstream tool needs one architecture, -thin writes a new thin file. It requires a universal input and an output path:
$ llvm-lipo-20 \
/path/to/program-universal.o \
-thin arm64 \
-output /path/to/program-arm64-thin.o
$ llvm-lipo-20 /path/to/program-arm64-thin.o -info
Non-fat file: /path/to/program-arm64-thin.o is architecture: arm64
This does not remove the arm64 slice from the original. It creates a separate file. Keep the original until the extracted file has passed the consumer's checks.
6. Replace a slice only with a verified input
-replace takes an architecture, a thin input file, and a universal input, then writes another universal output. Verify the replacement file before using it:
$ llvm-lipo-20 /path/to/program-arm64-new.o -verify_arch arm64
$ llvm-lipo-20 \
/path/to/program-universal.o \
-replace arm64 /path/to/program-arm64-new.o \
-output /path/to/program-universal-new.o
$ llvm-lipo-20 /path/to/program-universal-new.o -verify_arch x86_64 arm64
Do not replace in place while diagnosing a build. A mistaken architecture, stale object, or incompatible ABI can make the output unusable, and llvm-lipo-20 cannot restore the previous file. Write a new name, compare or test it, then promote it with your normal atomic deployment procedure. If you did overwrite a file accidentally, restore it from the build artefact or backup; there is no undo option in llvm-lipo-20.
7. Use segment alignment only when required
-segalign applies to -create and -replace. Its value is hexadecimal and must be a non-zero power of two. In this installed version, write the hexadecimal digits without a 0x prefix:
$ llvm-lipo-20 \
/path/to/program-x86_64.o \
/path/to/program-arm64.o \
-create \
-segalign x86_64 1000 \
-output /path/to/program-aligned.o
Here 1000 means hexadecimal 0x1000. Do not add alignment speculatively. Use the value required by the consumer or by the object-producing tool, then inspect and test the resulting file. A value such as 3 is rejected because it is not a power of two.
Common failure traps
- Options before inputs: the installed command's usage is input files followed by options. If it says that no input file was specified, move the file names before
-info,-archsor-verify_arch. - Missing output:
-create,-thinand-replacerequire-output. Choose a new path while testing. - Wrong file kind:
llvm-lipo-20is for Mach-O. Check the producer and target instead of trying to force an ELF file into a universal file. - Assuming success proves compatibility: an architecture list proves container structure, not that the slices link, load, or behave correctly. Run the relevant linker, loader and application tests afterwards.
Done means
- The installed command reports LLVM 20.1.8 and the expected binary path.
- Each thin input reports the intended Mach-O architecture.
- The universal output reports every required architecture with
-infoor-archs. - Automation uses
-verify_archexit status rather than parsing display text. - Thin extraction and slice replacement write new paths until the result is tested.
- Any segment alignment was required, hexadecimal, and a non-zero power of two.