Build a Darwin Static Library with llvm-libtool-darwin

llvm-libtool-darwin-18 turns Mach-O object files into a Darwin static archive, dodging the classic Linux trap of feeding it an ELF object. You will inspect the result and know exactly why that trap catches people. The examples use llvm-libtool-darwin-18 from Ubuntu package llvm-18, version 18.1.3. Allow about fifteen minutes if LLVM and Clang are already installed.

You need a shell, llvm-libtool-darwin-18, and object files compiled for a Darwin target. This guide creates a small archive in a working directory. It does not install anything, write to a system library directory or require sudo.

1. Check the installed tool

Confirm the binary and its version before relying on examples. These are ordinary read-only commands:

$ command -v llvm-libtool-darwin-18
/usr/bin/llvm-libtool-darwin-18
$ llvm-libtool-darwin-18 -version
Ubuntu LLVM version 18.1.3
  Optimized build.

The local manual describes this program as a drop-in replacement for cctools libtool in most scenarios. The installed command accepts both -version and -V. The package version matters: later LLVM releases can add options or change diagnostics.

Checkpoint: If the command is missing, stop here and install the LLVM package through your normal package-management process. Do not copy a random binary into /usr/bin.

2. Produce a Mach-O object

On Linux, an unqualified Clang compile normally produces an ELF object. That is the wrong input format for this Darwin-specific archiver. Compile a source file for a Darwin target instead:

$ mkdir -p build-darwin
$ printf '%s\n' 'int answer(void) { return 42; }' > build-darwin/answer.c
$ clang -target x86_64-apple-darwin -c build-darwin/answer.c -o build-darwin/answer.o
$ file build-darwin/answer.o
build-darwin/answer.o: Mach-O 64-bit x86_64 object

This example only compiles a function and does not include a system header, so it does not need a Darwin SDK. Real programs may need an SDK, headers and libraries for the target platform. The target triple selects the object format and architecture; it does not make a complete Darwin application by itself.

Do not delete an existing source or object file to make this example fit. Use a new directory, or choose output names that cannot collide with work you care about.

3. Create the static archive

Pass the object file after -o. The -static option requests a static library:

$ llvm-libtool-darwin-18 -static -o build-darwin/libanswer.a build-darwin/answer.o
$ printf 'exit status: %s\n' "$?"
exit status: 0
$ file build-darwin/libanswer.a
build-darwin/libanswer.a: current ar archive

The output name is required exactly once. A successful exit status means the archive operation completed. It does not prove that the library contains every symbol your eventual link needs.

The command's default -D mode uses zero for timestamps and user and group IDs. That makes archive metadata stable for repeatable builds. Use -U only when you deliberately need actual timestamps and IDs. Changing to -U makes otherwise identical archives vary with their build environment.

Checkpoint: The archive should exist and the command should return status 0. If a previous libanswer.a is valuable, write to libanswer.a.new, inspect it, then rename it only after the new archive is known to be sound. A shell redirection or a tool output option can replace an existing file without asking.

4. Inspect the members and architecture

Use an archiver or LLVM inspection tool to check what was written. These commands only read the new archive:

$ ar t build-darwin/libanswer.a
__.SYMDEF
answer.o
$ llvm-nm build-darwin/libanswer.a
0000000000000000 T _answer

The exact symbol addresses can differ. The useful checks are that the archive contains the expected object and that answer is present as a defined text symbol. Darwin symbol names commonly have a leading underscore, which is why the inspection output shows _answer.

For a multi-architecture build, use -arch_only ARCHITECTURE to build a static library from only the named architecture, ignoring other architectures in the inputs. Supply the architecture name used by your object files. Do not guess: check each input with file first, and keep architecture selection consistent across the build.

5. Use a file list when inputs are numerous

-filelist reads one input filename per line. Whitespace is part of a filename, so this format is safer than trying to split a shell variable on spaces:

$ printf '%s\n' build-darwin/answer.o > build-darwin/objects.list
$ llvm-libtool-darwin-18 -static -filelist build-darwin/objects.list -o build-darwin/libanswer-from-list.a
$ ar t build-darwin/libanswer-from-list.a
__.SYMDEF
answer.o

You can add a directory prefix as -filelist listfile,dirname. The prefix is prepended to every filename in the list. Review the list before running the command, especially if it was generated by another build step. A filename containing a newline cannot be represented unambiguously by this one-name-per-line format.

6. Diagnose input and search-path failures

If the command reports format not supported, inspect the input. An ELF object from a normal Linux compile is not a Darwin object:

$ clang -c build-darwin/answer.c -o build-darwin/answer-linux.o
$ file build-darwin/answer-linux.o
build-darwin/answer-linux.o: ELF 64-bit LSB relocatable

Compile for the intended Darwin target again, then pass the resulting Mach-O object. This tool does not convert ELF into Mach-O.

When using -l NAME, the tool searches for libNAME.a. If NAME ends in .o, it searches for that exact object name instead. Add search directories with -L DIRECTORY; they are searched in the order supplied, before the manual's default paths /lib, /usr/lib and /usr/local/lib. Review every -L path in a build script because search order can select a different library than expected.

Warnings about input files with no symbols are normally useful. -no_warning_for_no_symbols suppresses them, while -warnings_as_errors makes any warning produce a non-zero status. Prefer fixing an unexpected empty object over hiding the warning.

Done means