lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) altarch = $(subst x86_64,amd64,$(subst aarch64,arm64,$1)) TOOLCHAIN_VOLUME := $(PWD):/home/build TOOLCHAIN_WORKDIR := /home/build DEFAULT_GOAL := $(or $(DEFAULT_GOAL),toolchain) ARCH := $(or $(ARCH),x86_64) TARGET := $(or $(TARGET),$(ARCH)) normarch = $(subst arm64,aarch64,$(subst amd64,x86_64,$1)) HOST_ARCH := $(call normarch,$(call lc,$(shell uname -m))) HOST_ARCH_ALT := $(call altarch,$(HOST_ARCH)) HOST_OS := $(call lc,$(shell uname -s)) PLATFORM := $(or $(PLATFORM),linux) NAME := $(shell basename $(shell git rev-parse --show-toplevel | tr A-Z a-z )) UID := $(shell id -u) GID := $(shell id -g) USER := $(UID):$(GID) USERNAME := $(shell whoami) HOSTNAME := $(shell uname -n) CPUS := $(shell docker run debian nproc) ARCHIVE_SOURCES := true GIT_REF := $(shell git log -1 --format=%H) GIT_AUTHOR := $(shell git log -1 --format=%an) GIT_KEY := $(shell git log -1 --format=%GP) GIT_TIMESTAMP := $(shell git log -1 --format=%cd --date=iso) , := , empty := space := $(empty) $(empty) ifeq ($(strip $(shell git status --porcelain 2>/dev/null)),) GIT_STATE=clean else GIT_STATE=dirty endif VERSION := $(shell TZ=UTC0 git show --quiet --date='format-local:%Y.%m.%d' --format="%cd") DIST_DIR := dist CONFIG_DIR := config CACHE_DIR_ROOT := cache FETCH_DIR := fetch ifeq ($(TARGET),$(ARCH)) CACHE_DIR := $(CACHE_DIR_ROOT)/$(TARGET) else CACHE_DIR := $(CACHE_DIR_ROOT)/$(TARGET)/$(ARCH) endif BIN_DIR := $(CACHE_DIR_ROOT)/bin SRC_DIR := src KEY_DIR := fetch/keys OUT_DIR := out IMAGE := toolchain/$(shell git ls-files -s $(CONFIG_DIR) | git hash-object --stdin) docker = docker PATH_PREFIX := /home/build/.local/bin:/home/build/$(CACHE_DIR)/bin:/home/build/$(OUT_DIR)/linux/x86_64 PREFIX := $(HOME)/.local XDG_CONFIG_HOME := $(HOME)/.config # MacOS users do not have a 'date' command that supports milliseconds # This is what we are forced to do. Other ideas welcome define epochms $$(python3 -c 'from time import time; print(int(round(time() * 1000)))') endef ifneq ($(TOOLCHAIN_PROFILE),false) TOOLCHAIN_PROFILE_DIR := .toolchain/profiles mkc := $(shell mkdir -p $(TOOLCHAIN_PROFILE_DIR)) ifndef TOOLCHAIN_PROFILE_RUNNING TOOLCHAIN_PROFILE_INIT := $(shell printf $(call epochms)) TOOLCHAIN_PROFILE_RUNNING := true TOOLCHAIN_PROFILE_FILE := \ $(TOOLCHAIN_PROFILE_DIR)/$(HOSTNAME)-$(USERNAME)-$(HOST_OS)-$(HOST_ARCH).$(shell date -u -d @$$(($(TOOLCHAIN_PROFILE_INIT) / 1000)) +%Y%m%dT%H%M%S).csv endif .PHONY: toolchain-profile toolchain-profile: $(call toolchain-profile-total) @echo Build times: @bash -c ' \ while IFS=, read -r target ms_start ms_stop; do \ ms_diff=$$(($$ms_stop - $$ms_start)); \ echo - $$target,$$(date -u -d @$$(( $$ms_diff / 1000 )) +%T); \ done < $(TOOLCHAIN_PROFILE_FILE)' \ | column -c 80 -s, -t @echo "Total: $$(date -u -d @$$(( $(TOOLCHAIN_PROFILE_TOTAL) / 1000 )) +%T)"; endif define toolchain-profile-total $(eval TOOLCHAIN_PROFILE_TOTAL=$(shell expr $(call epochms) - $(TOOLCHAIN_PROFILE_INIT)) ) endef define toolchain-profile-tracked $(eval TOOLCHAIN_PROFILE_TRACKED=$(shell cat $(TOOLCHAIN_PROFILE_FILE) | cut -d ',' -f2 | awk '{ sum += $$1 } END { print sum }')) endef define toolchain-profile-untracked $(eval TOOLCHAIN_PROFILE_UNTRACKED=$(shell expr $(TOOLCHAIN_PROFILE_TOTAL) - $(TOOLCHAIN_PROFILE_TRACKED)) ) endef define toolchain-profile-start printf "%s,$(call epochms),\n" "$@" >> $(TOOLCHAIN_PROFILE_FILE); endef define toolchain-profile-stop tmpfile=$$(mktemp -q "$(TOOLCHAIN_PROFILE_DIR)/tmp.XXXXXXXXX") \ && cp $(TOOLCHAIN_PROFILE_FILE) $$tmpfile \ && awk \ -v ms="$(call epochms)" \ '/^$(@),/ {$$0=$$0ms} 1' \ $$tmpfile \ > $(TOOLCHAIN_PROFILE_FILE) endef export include $(CONFIG_DIR)/make.env export $(shell sed 's/=.*//' $(CONFIG_DIR)/make.env) ## Use env vars from existing release if present ifeq ($(TOOLCHAIN_REPRODUCE),true) include $(DIST_DIR)/release.env export endif executables = $(docker) git git-lfs patch .PHONY: toolchain toolchain: \ $(CACHE_DIR) \ $(FETCH_DIR) \ $(BIN_DIR) \ $(OUT_DIR) \ $(CACHE_DIR_ROOT)/toolchain.state \ $(CACHE_DIR_ROOT)/container.env # Launch a shell inside the toolchain container .PHONY: toolchain-shell toolchain-shell: toolchain $(call toolchain,bash --norc,--interactive) .PHONY: toolchain-update toolchain-update: rm -rf \ $(CONFIG_DIR)/apt-pins-x86_64.list \ $(CONFIG_DIR)/apt-sources-x86_64.list \ $(CONFIG_DIR)/apt-hashes-x86_64.list \ $(FETCH_DIR)/apt $(MAKE) $(CONFIG_DIR)/apt-hashes-x86_64.list .PHONY: toolchain-restore-mtime toolchain-restore-mtime: $(call toolchain-profile-start) bash -c '\ for d in $$(git ls-files | xargs -n 1 dirname | uniq); do \ mkdir -p "$$d"; \ done; \ for f in $$((git ls-files --modified; git ls-files) | sort | uniq -u); do \ ( test -f "$$f" || test -d "$$f" ) \ && touch -t \ $$(git log \ --pretty=format:%cd \ --date=format:%Y%m%d%H%M.%S \ -1 "HEAD" -- "$$f"\ ) "$$f"; \ done; \ ' $(call toolchain-profile-stop) .PHONY: toolchain-dist-cache toolchain-dist-cache: mkdir -p $(OUT_DIR) cp -Rp $(DIST_DIR)/* $(OUT_DIR)/ $(CONFIG_DIR)/apt-base.list: touch $(CONFIG_DIR)/apt-base.list # Regenerate toolchain dependency packages to latest versions $(CONFIG_DIR)/apt-pins-x86_64.list \ $(CONFIG_DIR)/apt-sources-x86_64.list \ $(CONFIG_DIR)/apt-hashes-x86_64.list: \ $(CONFIG_DIR)/apt-base.list $(call toolchain-profile-start) mkdir -p $(FETCH_DIR)/apt \ && docker run \ --rm \ --tty \ --platform=linux/$(ARCH) \ --env LOCAL_USER=$(UID):$(GID) \ --volume $(PWD)/$(CONFIG_DIR):/config \ --volume $(PWD)/$(SRC_DIR)/toolchain/scripts:/usr/local/bin \ --cpus $(CPUS) \ --volume $(TOOLCHAIN_VOLUME) \ --workdir $(TOOLCHAIN_WORKDIR) \ debian@sha256:$(DEBIAN_HASH) \ /usr/local/bin/packages-update $(call toolchain-profile-stop) # Pin all packages in toolchain container to latest versions $(FETCH_DIR)/apt/Packages.bz2: $(CONFIG_DIR)/apt-hashes-x86_64.list $(call toolchain-profile-start) docker run \ --rm \ --tty \ --platform=linux/$(ARCH) \ --env LOCAL_USER=$(UID):$(GID) \ --env FETCH_DIR="$(FETCH_DIR)" \ --env ARCHIVE_SOURCES=$(ARCHIVE_SOURCES) \ --volume $(PWD)/$(CONFIG_DIR):/config \ --volume $(PWD)/$(SRC_DIR)/toolchain/scripts:/usr/local/bin \ --volume $(PWD)/$(FETCH_DIR):/fetch \ --cpus $(CPUS) \ --volume $(TOOLCHAIN_VOLUME) \ --workdir $(TOOLCHAIN_WORKDIR) \ debian@sha256:$(DEBIAN_HASH) \ /usr/local/bin/packages-fetch $(call toolchain-profile-stop) .PHONY: toolchain-clean toolchain-clean: $(call toolchain-profile-start) if [ -d "$(CACHE_DIR_ROOT)" ]; then \ chmod -R u+w $(CACHE_DIR_ROOT); \ rm -rf $(CACHE_DIR_ROOT); \ fi if [ -d "$(OUT_DIR)" ]; then \ rm -rf $(OUT_DIR); \ fi docker image rm -f $(IMAGE) || : $(call toolchain-profile-stop) .PHONY: toolchain-reproduce toolchain-reproduce: toolchain-clean mkdir -p $(OUT_DIR) $(MAKE) TOOLCHAIN_REPRODUCE="true" diff -q $(OUT_DIR) $(DIST_DIR) \ && echo "Success: $(OUT_DIR) and $(DIST_DIR) are identical" .PHONY: toolchain-dist toolchain-dist: git ls-files -o --exclude-standard | grep . \ && { echo "Error: Git has untracked files present"; exit 1; } || : git diff --name-only | grep . \ && { echo "Error: Git has unstaged changes present"; exit 1; } || : $(MAKE) toolchain-restore-mtime toolchain-clean toolchain-dist-cache default cp -Rp $(OUT_DIR)/* $(DIST_DIR)/ $(BIN_DIR): mkdir -p $@ $(CACHE_DIR): mkdir -p $@ $(FETCH_DIR): mkdir -p $@ $(OUT_DIR): mkdir -p $@ $(CACHE_DIR_ROOT)/container.env: \ $(CONFIG_DIR)/make.env \ $(CACHE_DIR_ROOT)/toolchain.state docker run \ --rm \ --env UID=$(UID) \ --env GID=$(GID) \ --env NAME="$(NAME)" \ --env IMAGE="$(IMAGE)" \ --env USER="$(USER)" \ --env ARCH="$(ARCH)" \ --env HOST_ARCH="$(HOST_ARCH)" \ --env HOST_ARCH_ALT="$(HOST_ARCH_ALT)" \ --env HOST_OS="$(HOST_OS)" \ --env PLATFORM="$(PLATFORM)" \ --env CPUS="$(CPUS)" \ --env TARGET="$(TARGET)" \ --env GIT_REF="$(GIT_REF)" \ --env GIT_AUTHOR="$(GIT_AUTHOR)" \ --env GIT_KEY="$(GIT_KEY)" \ --env GIT_TIMESTAMP="$(GIT_TIMESTAMP)" \ --env VERSION="$(VERSION)" \ --env DIST_DIR="$(DIST_DIR)" \ --env FETCH_DIR="$(FETCH_DIR)" \ --env KEY_DIR="$(KEY_DIR)" \ --env BIN_DIR="$(BIN_DIR)" \ --env OUT_DIR="$(OUT_DIR)" \ --env SRC_DIR="$(SRC_DIR)" \ --env CACHE_DIR="$(CACHE_DIR)" \ --env CACHE_DIR_ROOT="$(CACHE_DIR_ROOT)" \ --env CONFIG_DIR="$(CONFIG_DIR)" \ --env TOOLCHAIN_VOLUME="$(TOOLCHAIN_VOLUME)" \ --env TOOLCHAIN_WORKDIR="$(TOOLCHAIN_WORKDIR)" \ --platform=linux/$(ARCH) \ --volume $(TOOLCHAIN_VOLUME) \ --workdir $(TOOLCHAIN_WORKDIR) \ $(shell cat cache/toolchain.state 2> /dev/null) \ $(SRC_DIR)/toolchain/scripts/environment > $@ $(CACHE_DIR_ROOT)/toolchain.tgz: \ $(CONFIG_DIR)/make.env \ $(SRC_DIR)/toolchain/Dockerfile \ $(CONFIG_DIR)/apt-base.list \ $(CONFIG_DIR)/apt-sources-$(ARCH).list \ $(CONFIG_DIR)/apt-pins-$(ARCH).list \ $(CONFIG_DIR)/apt-hashes-$(ARCH).list \ | $(FETCH_DIR)/apt/Packages.bz2 $(call toolchain-profile-start) mkdir -p $(CACHE_DIR) DOCKER_BUILDKIT=1 \ docker build \ --tag $(IMAGE) \ --build-arg DEBIAN_HASH=$(DEBIAN_HASH) \ --build-arg CONFIG_DIR=$(CONFIG_DIR) \ --build-arg FETCH_DIR=$(PWD)/$(FETCH_DIR) \ --build-arg SCRIPTS_DIR=$(SRC_DIR)/toolchain/scripts \ --platform=linux/$(ARCH) \ -f $(SRC_DIR)/toolchain/Dockerfile \ . docker save "$(IMAGE)" | gzip > "$@" $(call toolchain-profile-stop) $(CACHE_DIR_ROOT)/toolchain.state: \ $(CACHE_DIR_ROOT)/toolchain.tgz $(call toolchain-profile-start) docker load -i $(CACHE_DIR_ROOT)/toolchain.tgz docker images --no-trunc --quiet $(IMAGE) > $@ $(call toolchain-profile-stop) $(OUT_DIR)/release.env: $(shell git ls-files) echo 'VERSION=$(VERSION)' > $(OUT_DIR)/release.env echo 'GIT_REF=$(GIT_REF)' >> $(OUT_DIR)/release.env echo 'GIT_AUTHOR=$(GIT_AUTHOR)' >> $(OUT_DIR)/release.env echo 'GIT_KEY=$(GIT_KEY)' >> $(OUT_DIR)/release.env echo 'GIT_TIMESTAMP=$(GIT_TIMESTAMP)' >> $(OUT_DIR)/release.env check_executables := $(foreach exec,$(executables),\$(if \ $(shell which $(exec)),some string,$(error "No $(exec) in PATH"))) define sha256_file $$(openssl sha256 $(1) | awk '{ print $$2}') endef define fetch_file bash -c " \ echo \"Fetching $(1)\" \ && curl \ --location $(1) \ --output $(CACHE_DIR)/$(notdir $@) \ && [[ "\""$(call sha256_file,$(CACHE_DIR)/$(notdir $@))"\"" == "\""$(2)"\"" ]] \ || { echo 'Error: Hash check failed'; exit 1; } \ && mv $(CACHE_DIR)/$(notdir $@) $@; \ " endef define git_archive $(call git_clone,$(CACHE_DIR)/$(notdir $@),$(1),$(2)) \ && tar \ -C $(CACHE_DIR)/$(notdir $@) \ --sort=name \ --mtime='@0' \ --owner=0 \ --group=0 \ --numeric-owner \ -cvf - \ . \ | gzip -n > $@ \ && rm -rf $(CACHE_DIR)/$(notdir $@) endef define git_clone [ -d $(1) ] || \ mkdir -p $(1).tmp && \ git -C $(1).tmp init && \ git -C $(1).tmp remote add origin $(2) && \ git -C $(1).tmp fetch origin $(3) && \ git -C $(1).tmp -c advice.detachedHead=false checkout $(3) && \ git -C $(1).tmp submodule update --init && \ git -C $(1).tmp rev-parse --verify HEAD | grep -q $(3) || { \ echo 'Error: Git ref/branch collision.'; exit 1; \ } && \ mv $(1).tmp $(1) endef define apply_patches [ -d $(2) ] && $(call toolchain," \ cd $(1); \ git restore .; \ find /$(2) -type f -iname '*.patch' -print0 \ | xargs -t -0 -n 1 patch -p1 --no-backup-if-mismatch -i ; \ ") endef define fetch_pgp_key mkdir -p $(KEY_DIR) && \ $(call toolchain,' \ for server in \ keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ hkp://p80.pool.sks-keyservers.net:80 \ ha.pool.sks-keyservers.net \ pgp.mit.edu \ ; do \ echo "Trying: $${server}"; \ gpg \ --keyserver "$${server}" \ --keyserver-options timeout=10 \ --recv-keys "$(1)" \ && break; \ done; \ gpg --export -a $(1) > $@; \ ') endef define toolchain ( test -f $(CACHE_DIR_ROOT)/toolchain.state || { \ echo "Error: toolchain.state not found. Check dependencies!"; \ exit 1; \ };) \ && docker run \ --rm \ --tty \ $(2) \ --env UID=$(UID) \ --env GID=$(GID) \ --env PATH_PREFIX=$(PATH_PREFIX) \ --platform=linux/$(ARCH) \ --privileged \ --cpus $(CPUS) \ --volume $(TOOLCHAIN_VOLUME) \ --workdir $(TOOLCHAIN_WORKDIR) \ --env-file=$(CACHE_DIR_ROOT)/container.env \ $$(cat $(CACHE_DIR_ROOT)/toolchain.state 2> /dev/null) \ $(SRC_DIR)/toolchain/scripts/host-env bash -c $(1) endef