Извините, но в 2025 году — это просто смешно:
$ time ./configure
...
./configure 13.80s user 12.72s system 69% cpu 38.018 total
$ time make -j48
...
make -j48 12.05s user 4.70s system 593% cpu 2.822 total
Я заплатил приличные деньги за 24 ядра CPU, а ./configure
умудряется грузить только 69% одного ядра! В результате этот рандомный проект конфигурится в 13.5 раз медленнее, чем потом реально собирается.
Назначение ./configure
— это просто много раз вызвать компилятор и проверить, какие тесты прошли. Типа: есть ли нужные заголовки, функции, структуры, поля — чтобы писать переносимый код. Эта задача идеально параллелится, но автотулы (autoconf, cmake, meson и прочие) до сих пор не умеют это делать.
Типичная структура configure скрипта выглядит так:
CFLAGS="-g"
if $CC $CFLAGS -Wall empty.c; then
CFLAGS="$CFLAGS -Wall"
fi
: >config.h
if $CC $CFLAGS have_statx.c; then
echo "#define HAVE_STATX 1" >>config.h
else
echo "#define HAVE_STATX 0" >>config.h
fi
...
То есть проверки идут последовательно. Хотя на практике их спокойно можно было бы гнать параллельно. Более того, инструмент для параллельного исполнения у нас уже есть — make!
Почему бы не использовать его? Идея простая: У нас будет специальный configure.mk
, который будет генерить Makefile
и config.h
через make -j
:
# configure.mk
# The default goal generates both outputs, and merges the logs together
config: Makefile config.h
cat Makefile.log config.h.log >$@.log
rm Makefile.log config.h.log
Проверки превращаются в независимые таргеты. Например:
# configure.mk
# Дефолтные значения на всякий случай:
CC ?= cc
CPPFLAGS ?= -D_GNU_SOURCE
CFLAGS ?= -g
LDFLAGS ?=
# Экспортируем их, чтобы избежать удаления обратных слешей:
export _CC=${CC}
export _CPPFLAGS=${CPPFLAGS}
export _CFLAGS=${CFLAGS}
export _LDFLAGS=${LDFLAGS}
#Генерируем Makefile:
Makefile:
printf 'CC := %s\n' "$$_CC" >$@
printf 'CPPFLAGS := %s\n' "$$_CPPFLAGS" >>$@
printf 'CFLAGS := %s\n' "$$_CFLAGS" >>$@
printf 'LDFLAGS := %s\n' "$$_LDFLAGS" >>$@
Экспортирование export сделано так, чтобы избежать удаления обратных слешей из вызовов типа таких:
$ ./configure CPPFLAGS='-DMACRO=\"string\"'
Теперь проверим поддержку флагов (-Wall, -pthread, и т.д.) с помощью небольшого скрипта flags.sh
:
#!/bin/sh
set -eu
VAR="$1"
FLAGS="$2"
shift 2
if "$@" $FLAGS; then
printf '%s += %s\n' "$VAR" "$FLAGS"
fi
Простой пример:
$ ./flags.sh CFLAGS -Wall cc empty.c
CFLAGS += -Wall
Скрипт выведет CFLAGS += -Wall
только если cc empty.c -Wall
завершится успешно.
Мы можем использовать такой подход для генерации некоторых фрагментов makefile, которые включают только поддерживаемые флаги.
ALL_FLAGS = ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}
# Запускаем компилятор с заданными флагами, отправляя
#
# - stdout в foo.mk (напр. CFLAGS += -flag)
# - stderr в foo.mk.log (напр. error: unrecognized command-line option ‘-flag’)
# - бинарники в foo.mk.out
# - а потом сразу их удаляем
TRY_CC = ${CC} ${ALL_FLAGS} empty.c -o $@.out >$@ 2>$@.log && rm -f $@.out $@.d
deps.mk:
./flags.sh CPPFLAGS "-MP -MD" ${TRY_CC}
Wall.mk:
./flags.sh CFLAGS -Wall ${TRY_CC}
pthread.mk:
./flags.sh CFLAGS -pthread ${TRY_CC}
bind-now.mk:
./flags.sh LDFLAGS -Wl,-z,now ${TRY_CC}
Каждый из этих таргетов генерит крошечный фрагмент мэйкфайла, отвечающий за один флаг и каждый из них может работать независимо, параллельно!
Как только тесты будут готовы, мы можем объединить их все в основной файл Makefile
и очистить мусор:
FLAGS := \
deps.mk \
Wall.mk \
pthread.mk \
bind-now.mk
Makefile: ${FLAGS}
printf 'CC := %s\n' "$$_CC" >$@
...
cat ${FLAGS} >>$@
cat ${FLAGS:%=%.log} >$@.log
rm ${FLAGS} ${FLAGS:%=%.log}
Осталось добавить в Makefile ту часть которая фактически собирает наше приложение. Мы можем написать простой main.mk
следующим образом:
#main.mk
OBJS := main.o
app: ${OBJS}
${CC} ${CFLAGS} ${LDFLAGS} ${OBJS} -o $@
${OBJS}:
${CC} ${CPPFLAGS} ${CFLAGS} -c ${@:.o=.c} -o $@
-include ${OBJS:.o=.d}
А затем добавить его в Makefile
после всех флагов:
Makefile: ${FLAGS}
...
cat main.mk >>$@
Ещё нам нужно сгенерировать config.h, который определяет макросы, сообщающие нам, существуют ли определенные библиотеки/заголовки/функции/поля структур и т.д.
Проверка фичи делается тоже через компиляцию микро-программок, например:
проверка statx() have_statx.c
:
#include <fcntl.h>
#include <sys/stat.h>
int main(void) {
struct statx stx;
return statx(AT_FDCWD, ".", 0, STATX_BTIME, &stx);
}
проверка st_birthtim() have_st_birthtim.c
:
#include <sys/stat.h>
int main(void) {
struct stat sb = {0};
return sb.st_birthtim.tv_sec;
}
А define.sh
превратит результат выполнения в макрос:
#!/bin/sh
set -eu
MACRO=$1
shift
if "$@"; then
printf '#define %s 1\n' "$MACRO"
else
printf '#define %s 0\n' "$MACRO"
fi
который сгенерирует что-то типа в зависимости от результата выполнения:
#define HAVE_STATX 1
#define HAVE_ST_BIRTHTIM 0
Мы можем использовать его в makefile вот так:
#configure.mk
# Use a recursive make to pick up our auto-detected *FLAGS from above
config.h: Makefile
+${MAKE} -f header.mk $@
#header.mk
# Get the final *FLAGS values from the Makefile
include Makefile
# We first generate a lot of small headers, before merging them into one big one
HEADERS := \
have_statx.h \
have_st_birthtim.h \
have_st_birthtimespec.h \
have___st_birthtim.h
# Strip .h and capitalize the macro name
MACRO = $$(printf '%s' ${@:.h=} | tr 'a-z' 'A-Z')
ALL_FLAGS = ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}
${HEADERS}:
./define.sh ${MACRO} ${CC} ${ALL_FLAGS} ${@:.h=.c} -o $@.out >$@ 2>$@.log
rm -f $@.out $@.d
И потом всё это склеить в config.h
с защитой от двойного включения вот так:
#header.mk
config.h: ${HEADERS}
printf '#ifndef CONFIG_H\n' >$@
printf '#define CONFIG_H\n' >>$@
cat ${HEADERS} >>$@
printf '#endif\n' >>$@
cat ${HEADERS:%=%.log} >$@.log
rm ${HEADERS} ${HEADERS:%=%.log}
В итоге, полноценный ./configure
превращается в просто:
#!/bin/sh
set -eu
# Guess a good number for make -j<N>
jobs() {
{
nproc \
|| sysctl -n hw.ncpu \
|| getconf _NPROCESSORS_ONLN \
|| echo 1
} 2>/dev/null
}
# Default to MAKE=make
MAKE="${MAKE-make}"
# Set MAKEFLAGS to -j$(jobs) if it's unset
export MAKEFLAGS="${MAKEFLAGS--j$(jobs)}"
$MAKE -r -f configure.mk "$@"
Я сделал рабочий пример на GitHub где все эти файлы представлены полностью — можете скопировать себе. Демо печатает время создания файла, если оно поймет как делать это на вашей системе.
Я также давно использую подобную сборку в своем проекте bfs, и разница в производительности колоссальная:
$ time ./configure
...
./configure 1.44s user 1.78s system 802% cpu 0.401 total
$ time make -j48
...
make -j48 1.89s user 0.64s system 817% cpu 0.310 total
Конечно, часть выигрыша приходит от того, что я просто уменьшил количество ненужных проверок, но загрузка CPU на 802% вместо 69% одного ядра — это уже не смешно, это настоящее ускорение.