# Copyright © 2025-2026 Apple Inc. and the container project authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Version and build configuration variables BUILD_CONFIGURATION ?= debug WARNINGS_AS_ERRORS ?= true SWIFT_CONFIGURATION := $(if $(filter-out false,$(WARNINGS_AS_ERRORS)),-Xswiftc -warnings-as-errors) # Code-coverage instrumentation, layered onto the shared build stages. Empty for # ordinary builds; the coverage-* targets opt in via a target-specific value so # only those goals compile instrumented binaries. COVERAGE_FLAG ?= export RELEASE_VERSION ?= $(shell git describe --tags --always) export GIT_COMMIT := $(shell git rev-parse HEAD) # Commonly used locations SWIFT := "/usr/bin/swift" # Shared swift build invocation; callers append --build-tests / --product / etc. SWIFT_BUILD = $(SWIFT) build -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) DEST_DIR ?= /usr/local/ ROOT_DIR := $(shell git rev-parse --show-toplevel) BUILD_BIN_DIR = $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) --show-bin-path) STAGING_DIR := bin/$(BUILD_CONFIGURATION)/staging/ PKG_PATH := bin/$(BUILD_CONFIGURATION)/container-installer-unsigned.pkg DSYM_DIR := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM DSYM_PATH := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM.zip CODESIGN_OPTS ?= --force --sign - --timestamp=none # Conditionally use a temporary data directory for integration tests SYSTEM_START_OPTS := ifneq ($(strip $(APP_ROOT)),) SYSTEM_START_OPTS += --app-root "$(strip $(APP_ROOT))" endif ifneq ($(strip $(LOG_ROOT)),) SYSTEM_START_OPTS += --log-root "$(strip $(LOG_ROOT))" endif MACOS_VERSION := $(shell sw_vers -productVersion) MACOS_MAJOR := $(shell echo $(MACOS_VERSION) | cut -d. -f1) SUDO ?= sudo .DEFAULT_GOAL := all include Protobuf.Makefile .PHONY: all all: container all: init-block .PHONY: build build: @echo Building container binaries... @$(SWIFT) --version @$(SWIFT_BUILD) .PHONY: build-tests # Shared build stage for every test target: builds the test bundle (and the # product binaries) once so the test targets can run with --skip-build. This is # a distinct target from `build` so `make all test` builds products and tests as # two separate steps rather than colliding on a single once-built target. # COVERAGE_FLAG instruments the binaries when set by the coverage-* targets. build-tests: @echo Building container binaries and tests... @$(SWIFT) --version @$(SWIFT_BUILD) --build-tests $(COVERAGE_FLAG) .PHONY: coverage-all coverage-all: build-tests @"$(MAKE)" BUILD_CONFIGURATION=$(BUILD_CONFIGURATION) DEST_DIR="$(ROOT_DIR)/" SUDO= install @"$(MAKE)" init-block .PHONY: cli cli: @echo Building container CLI... @$(SWIFT) --version @$(SWIFT_BUILD) --product container @echo Installing container CLI to bin/... @mkdir -p bin @install "$(BUILD_BIN_DIR)/container" "bin/container" .PHONY: container # Install binaries under project directory container: build @"$(MAKE)" BUILD_CONFIGURATION=$(BUILD_CONFIGURATION) DEST_DIR="$(ROOT_DIR)/" SUDO= install .PHONY: release release: BUILD_CONFIGURATION = release release: all .PHONY: init-block init-block: @echo Building initfs if containerization is in edit mode @scripts/install-init.sh $(SYSTEM_START_OPTS) .PHONY: install install: installer-pkg @echo Installing container installer package @if [ -z "$(SUDO)" ] ; then \ temp_dir=$$(mktemp -d) ; \ xar -xf $(PKG_PATH) -C $${temp_dir} ; \ (cd "$(DEST_DIR)" && pax -rz -f $${temp_dir}/Payload) ; \ rm -rf $${temp_dir} ; \ else \ $(SUDO) installer -pkg $(PKG_PATH) -target / ; \ fi $(STAGING_DIR): @echo Installing container binaries from "$(BUILD_BIN_DIR)" into "$(STAGING_DIR)"... @rm -rf "$(STAGING_DIR)" @mkdir -p "$(join $(STAGING_DIR), bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources)" @install "$(BUILD_BIN_DIR)/container" "$(join $(STAGING_DIR), bin/container)" @install "$(BUILD_BIN_DIR)/container-apiserver" "$(join $(STAGING_DIR), bin/container-apiserver)" @install "$(BUILD_BIN_DIR)/container-runtime-linux" "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin/container-runtime-linux)" @install Sources/Plugins/RuntimeLinux/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/config.toml)" @install "$(BUILD_BIN_DIR)/container-network-vmnet" "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)" @install Sources/Plugins/NetworkVmnet/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/config.toml)" @install "$(BUILD_BIN_DIR)/container-core-images" "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)" @install Sources/Plugins/CoreImages/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/config.toml)" @install "$(BUILD_BIN_DIR)/machine-apiserver" "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)" @install Sources/Plugins/MachineAPIServer/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/config.toml)" @install Sources/Plugins/MachineAPIServer/Resources/init "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources/init)" @install Sources/Plugins/MachineAPIServer/Resources/create-user.sh "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources/create-user.sh)" @echo Install update script @install scripts/update-container.sh "$(join $(STAGING_DIR), bin/update-container.sh)" @echo Install uninstaller script @install scripts/uninstall-container.sh "$(join $(STAGING_DIR), bin/uninstall-container.sh)" .PHONY: installer-pkg installer-pkg: $(STAGING_DIR) @echo Signing container binaries... @codesign $(CODESIGN_OPTS) --identifier com.apple.container.cli "$(join $(STAGING_DIR), bin/container)" @codesign $(CODESIGN_OPTS) --identifier com.apple.container.apiserver "$(join $(STAGING_DIR), bin/container-apiserver)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-runtime-linux.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin/container-runtime-linux)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-network-vmnet.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)" @echo Creating application installer @pkgbuild --root "$(STAGING_DIR)" --identifier com.apple.container-installer --install-location /usr/local --version ${RELEASE_VERSION} $(PKG_PATH) @rm -rf "$(STAGING_DIR)" .PHONY: dsym dsym: @echo Copying debug symbols... @rm -rf "$(DSYM_DIR)" @mkdir -p "$(DSYM_DIR)" @cp -a "$(BUILD_BIN_DIR)/container-runtime-linux.dSYM" "$(DSYM_DIR)" @cp -a "$(BUILD_BIN_DIR)/container-network-vmnet.dSYM" "$(DSYM_DIR)" @cp -a "$(BUILD_BIN_DIR)/container-core-images.dSYM" "$(DSYM_DIR)" @cp -a "$(BUILD_BIN_DIR)/container-apiserver.dSYM" "$(DSYM_DIR)" @cp -a "$(BUILD_BIN_DIR)/container.dSYM" "$(DSYM_DIR)" @echo Packaging the debug symbols... @(cd "$(dir $(DSYM_DIR))" ; zip -r $(notdir $(DSYM_PATH)) $(notdir $(DSYM_DIR))) .PHONY: test test: build-tests @$(SWIFT) test --skip-build -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI --skip IntegrationTests .PHONY: install-kernel install-kernel: @echo Stopping system before installing kernel @bin/container system stop || true @echo Starting system to install kernel @bin/container --debug system start --timeout 60 --enable-kernel-install $(SYSTEM_START_OPTS) # Coverage report generation helpers # Directory that swift test spits out raw coverage data COV_DATA_DIR = $(shell $(SWIFT) test --show-coverage-path | xargs dirname) COV_REPORT_FILE = $(ROOT_DIR)/code-coverage-report COVERAGE_OUTPUT_DIR := $(ROOT_DIR)/coverage-reports TEST_BINARY = $(BUILD_BIN_DIR)/containerPackageTests.xctest/Contents/MacOS/containerPackageTests # All product binaries that may be instrumented for coverage. # Used as additional -object args to llvm-cov for integration/combined reports. COV_BINARIES := \ $(BUILD_BIN_DIR)/container \ $(BUILD_BIN_DIR)/container-apiserver \ $(BUILD_BIN_DIR)/container-runtime-linux \ $(BUILD_BIN_DIR)/container-network-vmnet \ $(BUILD_BIN_DIR)/container-core-images \ $(BUILD_BIN_DIR)/machine-apiserver COV_OBJECT_FLAGS := $(patsubst %,-object %,$(COV_BINARIES)) # Set of files we do not want to get caught in the coverage generation LLVM_COV_IGNORE := \ --ignore-filename-regex=".build/" \ --ignore-filename-regex="/Tests/" \ --ignore-filename-regex="/ContainerTestSupport/" \ --ignore-filename-regex=".pb.swift" \ --ignore-filename-regex=".proto" \ --ignore-filename-regex=".grpc.swift" # Generate JSON + HTML coverage reports and a coverage-percent.txt from a profdata file. # $(1) = profdata path, $(2) = tier name (unit/integration/combined), $(3) = additional -object flags (optional) define GENERATE_COV_REPORTS @echo Exporting $(2) coverage JSON... @xcrun llvm-cov export --compilation-dir=`pwd` \ -instr-profile=$(1) \ $(LLVM_COV_IGNORE) \ $(TEST_BINARY) $(3) > $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-summary.json @echo Generating $(2) coverage HTML report... @xcrun llvm-cov show --compilation-dir=`pwd` --format=html \ -instr-profile=$(1) \ $(LLVM_COV_IGNORE) \ -output-dir=$(COVERAGE_OUTPUT_DIR)/$(2)/html \ $(TEST_BINARY) $(3) @echo Extracting $(2) coverage percentages... @jq -r '.data[0].totals as $$t | \ "Coverage summary:", \ " lines: \($$t.lines.percent | . * 100 | round | . / 100)% (\($$t.lines.covered) of \($$t.lines.count))", \ " functions: \($$t.functions.percent | . * 100 | round | . / 100)% (\($$t.functions.covered) of \($$t.functions.count))", \ " regions: \($$t.regions.percent | . * 100 | round | . / 100)% (\($$t.regions.covered) of \($$t.regions.count))"' \ $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-summary.json > $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-percent.txt @echo "-- $(2) coverage --" @cat $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-percent.txt endef # PARALLEL_WIDTH controls --experimental-maximum-parallelization-width for the # concurrent pass. WARMUP_FILTER, CONCURRENT_FILTER, and SERIAL_FILTER select # the three phases. PARALLEL_WIDTH ?= $(shell sysctl -n hw.physicalcpu) WARMUP_FILTER = ImageWarmup/ # Concurrent suites: Test*.swift files whose names do NOT end in Serial. CONCURRENT_TEST_SUITES ?= $(sort $(addsuffix /,$(basename $(notdir \ $(shell find Tests/IntegrationTests -name 'Test*.swift' \ ! -name '*Serial.swift' 2>/dev/null))))) CONCURRENT_FILTER = $(subst $(space),|,$(strip $(CONCURRENT_TEST_SUITES))) # Serial suites: Test*.swift files whose names end in Serial. SERIAL_TEST_SUITES ?= $(sort $(addsuffix /,$(basename $(notdir \ $(shell find Tests/IntegrationTests -name 'Test*Serial.swift' 2>/dev/null))))) SERIAL_FILTER = $(subst $(space),|,$(strip $(SERIAL_TEST_SUITES))) INTEGRATION_SWIFT_EXTRA ?= INTEGRATION_POST_TEST ?= # Environment prefix applied to the `container system start` invocation. Empty for # ordinary runs; coverage runs set LLVM_PROFILE_FILE here so launchd-managed helper # (XPC service) processes emit their own profraw data. INTEGRATION_PROFILE_ENV ?= PRESERVE_KERNELS ?= false # Default scratch root under the project directory so container build can access context # subdirectories (macOS restricts access to /var/folders from the container binary). # Override with SCRATCH_ROOT=/your/path on the command line. SCRATCH_ROOT ?= $(ROOT_DIR)/.test-scratch define RUN_INTEGRATION @echo Ensuring apiserver stopped before the CLI integration tests... @bin/container system stop && sleep 3 && scripts/ensure-container-stopped.sh @if [ -n "$(APP_ROOT)" ]; then \ mkdir -p $(APP_ROOT) ; \ if [ "$(PRESERVE_KERNELS)" = "true" ]; then \ echo "Clearing application data under $(APP_ROOT) (preserving kernels)..." ; \ find "$(APP_ROOT)" -mindepth 1 -maxdepth 1 ! -name kernels -exec rm -rf {} + ; \ else \ echo "Clearing application data under $(APP_ROOT)..." ; \ find "$(APP_ROOT)" -mindepth 1 -maxdepth 1 -exec rm -rf {} + ; \ fi ; \ fi @echo Running the integration tests... @$(INTEGRATION_PROFILE_ENV) bin/container --debug system start --timeout 60 --enable-kernel-install $(SYSTEM_START_OPTS) && \ { \ CLITEST_LOG_ROOT=$(LOG_ROOT) && export CLITEST_LOG_ROOT ; \ CLITEST_SCRATCH_ROOT=$(SCRATCH_ROOT) && export CLITEST_SCRATCH_ROOT ; \ CONTAINER_CLI_PATH=$(ROOT_DIR)/bin/container && export CONTAINER_CLI_PATH ; \ echo "==> Warmup pass" && \ $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter "$(WARMUP_FILTER)" && \ echo "==> Concurrent pass (width=$(PARALLEL_WIDTH))" && \ $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --experimental-maximum-parallelization-width $(PARALLEL_WIDTH) --filter "$(CONCURRENT_FILTER)" && \ echo "==> Global pass (serial)" && \ $(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --experimental-maximum-parallelization-width 1 --filter "$(SERIAL_FILTER)" ; \ exit_code=$$? ; \ $(INTEGRATION_POST_TEST) \ echo Ensuring apiserver stopped after the CLI integration tests ; \ scripts/ensure-container-stopped.sh ; \ exit $${exit_code} ; \ } endef .PHONY: integration integration: init-block $(RUN_INTEGRATION) .PHONY: coverage-integration coverage-integration: INTEGRATION_SWIFT_EXTRA = --skip-build --enable-code-coverage coverage-integration: INTEGRATION_POST_TEST = cp $(COV_DATA_DIR)/*.profraw $(COVERAGE_OUTPUT_DIR)/integration/ || true ; # Continuous mode (%c) mmaps the profraw and syncs counters live. The XPC helper # services are torn down by `launchctl bootout` (SIGTERM/SIGKILL) rather than # exiting cleanly, so a non-continuous profile (written by an atexit handler that # never runs on SIGKILL) would lose the helpers' counters. %p-%m keeps each # process/module profile in its own file so they don't collide. coverage-integration: INTEGRATION_PROFILE_ENV = LLVM_PROFILE_FILE=$(COVERAGE_OUTPUT_DIR)/integration/%p-%m%c.profraw coverage-integration: coverage-all @mkdir -p $(COVERAGE_OUTPUT_DIR)/integration @rm -f $(COVERAGE_OUTPUT_DIR)/integration/*.profraw $(RUN_INTEGRATION) @echo Merging integration coverage profdata... @xcrun llvm-profdata merge -sparse $(COVERAGE_OUTPUT_DIR)/integration/*.profraw -o $(COVERAGE_OUTPUT_DIR)/integration/default.profdata $(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/integration/default.profdata,integration,$(COV_OBJECT_FLAGS)) empty := space := $(empty) $(empty) # Opt the coverage targets in to instrumentation. The value propagates to the # shared build-tests target so compilation is instrumented when necessary. coverage coverage-all coverage-unit coverage-integration: COVERAGE_FLAG = --enable-code-coverage -Xswiftc -DCONTAINER_COVERAGE .PHONY: coverage # Merge the per-tier profdata from coverage-unit and coverage-integration into a # combined report. Each prerequisite target produces its own tier report first. coverage: coverage-unit coverage-integration @echo Merging combined coverage profdata... @mkdir -p $(COVERAGE_OUTPUT_DIR)/combined @xcrun llvm-profdata merge -sparse \ $(COVERAGE_OUTPUT_DIR)/unit/default.profdata \ $(COVERAGE_OUTPUT_DIR)/integration/default.profdata \ -o $(COVERAGE_OUTPUT_DIR)/combined/default.profdata $(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/combined/default.profdata,combined,$(COV_OBJECT_FLAGS)) .PHONY: coverage-unit coverage-unit: build-tests @echo Running unit test coverage... @rm -f $(COV_DATA_DIR)/*.profraw @mkdir -p $(COVERAGE_OUTPUT_DIR)/unit @$(SWIFT) test --skip-build --enable-code-coverage -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI --skip IntegrationTests @echo Merging unit coverage profdata... @xcrun llvm-profdata merge -sparse $(COV_DATA_DIR)/*.profraw -o $(COVERAGE_OUTPUT_DIR)/unit/default.profdata $(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/unit/default.profdata,unit) .PHONY: fmt fmt: swift-fmt update-licenses .PHONY: check check: swift-fmt-check check-licenses .PHONY: swift-fmt SWIFT_SRC = $(shell find . -type f -name '*.swift' -not -path "*/.*" -not -path "*.pb.swift" -not -path "*.grpc.swift" -not -path "*/checkouts/*") swift-fmt: @echo Applying the standard code formatting... @$(SWIFT) format --recursive --configuration .swift-format -i $(SWIFT_SRC) swift-fmt-check: @echo Applying the standard code formatting... @$(SWIFT) format lint --recursive --strict --configuration .swift-format-nolint $(SWIFT_SRC) .PHONY: update-licenses update-licenses: @echo Updating license headers... @./scripts/ensure-hawkeye-exists.sh @.local/bin/hawkeye format --fail-if-unknown --fail-if-updated false .PHONY: check-licenses check-licenses: @echo Checking license headers existence in source files... @./scripts/ensure-hawkeye-exists.sh @.local/bin/hawkeye check --fail-if-unknown .PHONY: pre-commit pre-commit: $(eval HOOKS_DIR := $(shell git rev-parse --git-path hooks)) cp scripts/pre-commit.fmt $(HOOKS_DIR)/ touch $(HOOKS_DIR)/pre-commit cat $(HOOKS_DIR)/pre-commit | grep -v 'hooks/pre-commit\.fmt' > /tmp/pre-commit.new || true echo 'PRECOMMIT_NOFMT=$${PRECOMMIT_NOFMT} $$(git rev-parse --git-path hooks/pre-commit.fmt)' >> /tmp/pre-commit.new mv /tmp/pre-commit.new $(HOOKS_DIR)/pre-commit chmod +x $(HOOKS_DIR)/pre-commit @./scripts/ensure-hawkeye-exists.sh .PHONY: serve-docs serve-docs: @echo 'to browse: open http://127.0.0.1:8000/container/documentation/' @rm -rf _serve @mkdir -p _serve @cp -a _site _serve/container @python3 -m http.server --bind 127.0.0.1 --directory ./_serve .PHONY: docs docs: @echo Updating API documentation... @rm -rf _site @scripts/make-docs.sh _site container .PHONY: cleancontent cleancontent: @bin/container system stop || true @echo Cleaning the content... @rm -rf ~/Library/Application\ Support/com.apple.container .PHONY: clean clean: @echo Cleaning build files... @rm -rf bin/ libexec/ @rm -rf _site _serve @rm -f $(COV_REPORT_FILE) @rm -rf $(COVERAGE_OUTPUT_DIR) @$(SWIFT) package clean