Makefile: support universal macOS builds via RUST_TARGETS

On macOS, Universal Binaries contain native executable code for
multiple architectures (such as Intel x86_64 and Apple Silicon arm64)
bundled into a single file. This is standard practice for macOS
distribution and CI packaging (such as internal distribution packages
or tooling like Burrito/Homebrew), allowing a single build artifact
to run natively across all Macs without Rosetta emulation or
maintaining separate packages.

When building Git C code for multiple architectures on macOS, the
Apple toolchain (clang) natively supports universal builds via
CFLAGS/LDFLAGS. When "-arch x86_64 -arch arm64" is passed, clang
automatically compiles and links universal binaries for all C object
files and executables out of the box.

Cargo and rustc, however, do not support multiple "-arch" flags or
emitting universal binaries in a single invocation. Instead, Cargo
requires invoking each target triple independently (e.g., passing
"--target x86_64-apple-darwin" and "--target aarch64-apple-darwin").

To bridge this gap when Rust is enabled:
  1. Allow specifying space-separated target triples in RUST_TARGETS.
  2. Introduce declarative pattern rules (target/%/...) to compile
     each target-specific library slice via Cargo.
  3. On macOS, if multiple targets are specified, use "lipo" (part of
     the mandatory Xcode Command Line Tools) to combine the resulting
     static libraries into target/release/libgitcore.a.

Once $(RUST_LIB) is compiled into a universal static archive, the
standard C linker seamlessly links it with the C object files to
produce universal Git executables.

Signed-off-by: Shardul Natu <snatu@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Shardul Natu
2026-07-08 03:21:18 +00:00
committed by Junio C Hamano
parent 87bd9bd40e
commit 924dfced6b

View File

@@ -500,6 +500,14 @@ include shared.mak
#
# Building Rust code requires Cargo.
#
# Define RUST_TARGETS if you want to cross-compile. If left unspecified, it uses
# the default Rust target on the system.
#
# On macOS, this supports specifying multiple targets, separated by a space.
# This will produce a Universal static library using `lipo`.
#
# Example: RUST_TARGETS="aarch64-apple-darwin x86_64-apple-darwin"
#
# == SHA-1 and SHA-256 defines ==
#
# === SHA-1 backend ===
@@ -941,16 +949,17 @@ LIB_FILE = libgit.a
ifndef NO_RUST
ifdef DEBUG
RUST_TARGET_DIR = target/debug
RUST_BUILD_CONFIG = debug
else
RUST_TARGET_DIR = target/release
RUST_BUILD_CONFIG = release
endif
ifeq ($(uname_S),Windows)
RUST_LIB = $(RUST_TARGET_DIR)/gitcore.lib
RUST_LIB_NAME = gitcore.lib
else
RUST_LIB = $(RUST_TARGET_DIR)/libgitcore.a
RUST_LIB_NAME = libgitcore.a
endif
RUST_LIB = target/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME)
endif
GITLIBS = common-main.o $(LIB_FILE)
@@ -3020,8 +3029,30 @@ $(LIB_FILE): $(LIB_OBJS)
$(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^
ifndef NO_RUST
ifeq ($(RUST_TARGETS),)
$(RUST_LIB): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
$(QUIET_CARGO)cargo build $(CARGO_ARGS)
else
ifneq ($(words $(RUST_TARGETS)),1)
ifneq ($(uname_S),Darwin)
$(error Building universal Rust libraries requires macOS (lipo is not available on $(uname_S)))
endif
endif
RUST_MEMBER_LIBS = $(foreach target,$(RUST_TARGETS),target/$(target)/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME))
$(RUST_MEMBER_LIBS): target/%/$(RUST_BUILD_CONFIG)/$(RUST_LIB_NAME): Cargo.toml $(RUST_SOURCES) $(LIB_FILE)
$(QUIET_CARGO)cargo build $(CARGO_ARGS) --target $*
$(RUST_LIB): $(RUST_MEMBER_LIBS)
$(call mkdir_p_parent_template)
$(QUIET_GEN)\
if test $(words $(RUST_TARGETS)) -gt 1; \
then \
lipo -create $^ -output $@; \
else \
cp $< $@; \
fi
endif
.PHONY: rust
rust: $(RUST_LIB)