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.
The route
Jump straight to the step you need, or tick off Done means at the end.
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
- The installed LLVM version and command path are known.
- Each input object is a Mach-O object for the intended architecture.
- The archive was created with one explicit
-opath and a zero exit status. fileandllvm-ar-20 -tconfirm a non-empty archive with the expected members.- Metadata policy is explicit:
-Dfor reproducible output or-Ufor actual timestamps and IDs. - No system directory or existing useful archive was overwritten without a deliberate recovery path.