Build a Darwin Static Library with llvm-libtool-darwin-20

llvm-libtool-darwin-20 compiles one Darwin object file into a static archive you can inspect without touching a system directory. The examples use LLVM 20.1.8 from Debian package llvm-20. Allow about fifteen minutes, including time to check that your input files target Darwin.

This is a cross-toolchain workflow on Linux. The library format is for Darwin, not for ordinary Linux linking. You need llvm-libtool-darwin-20, a compiler that can emit Darwin object files, and the file utility. The build steps are unprivileged. Do not use sudo to write into a working directory.

1. Check the installed tool

Confirm the executable and version before relying on examples from another LLVM release:

$ command -v llvm-libtool-darwin-20
/usr/bin/llvm-libtool-darwin-20
$ dpkg-query -W -f='${Package} ${Version}\n' llvm-20
llvm-20 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139
$ llvm-libtool-darwin-20 --version
Ubuntu LLVM version 20.1.8
  Optimized build.

The installed command accepts --version as well as the documented -version. The latter prints the version and exits. -V is different: it displays the version and carries out the requested operation, so reserve it for a command that is already ready to run.

Checkpoint: If the executable is missing, stop here and install the package through your normal package-management process. A missing command is not a reason to copy a library tool from another host.

2. Compile a Darwin object

Make a small source file in a temporary build directory and compile it for a Darwin target. The target triple is an example; choose the architecture and deployment target that match the software you are building:

$ build_dir=$(mktemp -d /tmp/llvm-libtool-darwin-XXXXXX)
$ printf '%s\n' 'int answer(void) { return 42; }' > "$build_dir/answer.c"
$ clang-20 -target x86_64-apple-darwin20 -c \
    "$build_dir/answer.c" -o "$build_dir/answer.o"
$ file "$build_dir/answer.o"
/tmp/llvm-libtool-darwin-XXXXXX/answer.o: Mach-O 64-bit x86_64 object

The temporary directory name varies, so the path in the output is illustrative. The useful check is Mach-O, followed by the architecture you selected. If file reports an ELF object, do not package it as though it were a Darwin object. Fix the compiler target first.

3. Create the static archive

Use -static and specify the output exactly once with -o. Put input object files after the options:

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

The archive is the result of this example. llvm-ar-20 -t only lists its members; it is not part of llvm-libtool-darwin-20. Keep the source object and archive in the temporary directory until the consuming link has succeeded.

Warning: -o names a file that can be replaced. If the destination already matters, choose a new name or move the finished archive into place only after verification. Redirection and careless output paths can destroy a previous build. To undo this example, remove the temporary build directory after checking its contents; no system library was changed.

4. Package several objects or a file list

Pass multiple object files as ordinary inputs:

$ llvm-libtool-darwin-20 -static \
    -o "$build_dir/libproject.a" \
    "$build_dir/answer.o" "$build_dir/other.o"

For a longer list, use -filelist. The list contains one file name per line. Whitespace is part of a file name, so do not format it as a shell-style list or indent its entries:

$ printf '%s\n' \
    "$build_dir/answer.o" \
    "$build_dir/other.o" > "$build_dir/objects.list"
$ llvm-libtool-darwin-20 -static \
    -filelist "$build_dir/objects.list" \
    -o "$build_dir/libproject.a"
$ llvm-ar-20 -t "$build_dir/libproject.a"
answer.o
other.o

You can append a comma and directory name to -filelist when every entry is relative to the same directory, for example -filelist objects.list,/path/to/objects. The directory is prepended to each listed name. Check the resulting member list when a build uses this form, because a misplaced comma or an unexpected whitespace character changes the input path.

5. Select libraries and architectures deliberately

-L DIR adds a library search directory. Search directories are considered in the order supplied, before the default locations /lib, /usr/lib and /usr/local/lib. -l NAME then searches for libNAME.a:

$ llvm-libtool-darwin-20 -static \
    -L "$build_dir" -l NAME \
    -o "$build_dir/libconsumer-input.a" OBJECT.o

Replace NAME and OBJECT.o with real inputs for your build. The named archive must exist and be suitable for the target. A successful exit status still needs an archive-member check. If the argument to -l ends in .o, the tool searches for that object name without adding lib or .a.

For a static archive, -arch_only ARCH keeps only the specified architecture and ignores other architectures in the input files. Use it when you have a deliberate universal or multi-architecture input policy. It does not convert an ELF object into Mach-O, and it does not replace a compiler target decision.

6. Keep archive metadata reproducible

-D is the default and uses zero for timestamps and user and group IDs. That makes repeated archives less dependent on the machine that produced them. Use -U only when actual timestamps and IDs are required by a specific integration:

$ llvm-libtool-darwin-20 -static -D \
    -o "$build_dir/libanswer-reproducible.a" "$build_dir/answer.o"
$ test -s "$build_dir/libanswer-reproducible.a" && echo 'archive is non-empty'
archive is non-empty

Do not add -U casually to a reproducible build. It changes metadata, not the object code, but it can make byte-for-byte comparisons differ.

7. Treat warnings and failures as build signals

The tool exits non-zero when an error occurs. -no_warning_for_no_symbols suppresses warnings for input files without symbols. -warnings_as_errors instead makes any emitted warning fail the command:

$ llvm-libtool-darwin-20 -static -warnings_as_errors \
    -o "$build_dir/libanswer.a" "$build_dir/answer.o"
$ printf 'exit status: %s\n' "$?"
exit status: 0

If a command fails, first check that -o appears exactly once, every listed input exists, the file-list entries have one filename per line, and the objects target Darwin. Check the archive member list again after correcting the command. Do not run the failing command as root: extra privilege does not repair a wrong architecture, missing input or unresolved -l name.

Done means