Pressing Run in Xcode hides a chain of decisions: which scheme to use, which targets to build and in what order, which build configuration, which device, where the output goes, and which of several hundred build settings apply. When a build behaves differently in Xcode than on the command line, or a setting you changed seems to do nothing, the cause is almost always one of those hidden choices.

This article makes each one visible with xcodebuild, the command-line tool behind Xcode’s Run button, using the todo-list folder from Build a Swift Package with Swift Package Manager on macOS. You will read the project’s schemes, targets, configurations and destinations from the terminal; find where each build lands; see which build settings apply and where each value came from; and put your own settings in .xcconfig files, plain-text files of build settings that Xcode layers under the project’s own. Those files give Debug builds the name To-Do Dev on the simulator’s home screen and Release builds the name To-Do, so you can tell at a glance which configuration you are running. The Makefile grows a CONFIG switch, a settings make target that prints the settings that matter, and an archive make target that produces the Release build you would later sign and upload. Every later article in this series uses this Makefile.

This article does not sign or export the archive. Signing attaches a certificate that identifies who built the app, which iOS requires before it installs an app on a device or Apple accepts an upload; exporting packages a signed app for the App Store or for testers. Both need a development team, which comes with an Apple Developer account, and they come later in the series. Step 10 marks where the line falls.

What you end up with

  • todo-list/Config/, three .xcconfig files: one each for Debug and Release, which the TodoList project uses as its base configurations, and a shared one both include.
  • A Makefile with make settings, make build CONFIG=Release, make run CONFIG=Release, and make archive, alongside the targets from the earlier articles.
  • An unsigned TodoList.xcarchive built from Release, with its debug symbols.

Prerequisites

  • The finished project from Build a Swift Package with Swift Package Manager on macOS: the todo-list folder with the TodoList app project, the TodoKit package, its Makefile, and the TodoList-iPhone simulator.
  • Xcode 27 or later on a Mac with Apple silicon. Validated on Xcode 27.1 (build 27A9275) with Swift 6.4, on macOS 27.0.1.

Every command runs from the todo-list folder.

Step 1: Ask the project what it can build

A build starts from a scheme: a named recipe that says which targets to build and which configuration to use for each action, such as Run, Test, or Archive. Before you can reason about a build, you need the names of the schemes, targets, and configurations, and xcodebuild -list prints all three. -quiet drops the lines about resolving packages, which include full paths on your disk.

xcodebuild -list -quiet -project TodoList/TodoList.xcodeproj
2026-10-23 14:48:15.742 xcodebuild[39731:558568] [MT] IDERunDestination: Supported platforms for the buildables in the current scheme is empty.
Information about project "TodoList":
    Targets:
        TodoList
        TodoListUITests

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and -scheme is not passed then "Release" is used.

    Schemes:
        TodoKit
        TodoList

The first line is a log message xcodebuild often prints on standard error, failed or not; it did not appear on every run while this article was validated. It is harmless, and the output blocks below leave it out. Three things in the rest matter later:

  • Targets are what the project builds: the app, and the UI tests that drive it. TodoKit is not listed because it is a package, not a target in this project; Xcode builds it first because the app depends on it.
  • Build configurations are named sets of build settings. Debug favors fast builds and debugging; Release favors fast, small apps. The note about Release applies only when you name neither a scheme nor a configuration, which the Makefile never does.
  • Schemes list TodoList, the one every make target uses, and TodoKit, which Xcode generated for the package so it can be built on its own. No scheme file is saved in the project folder; Xcode and xcodebuild generate both whenever they open the project.

The TodoList scheme uses Debug for Run and Test and Release for Archive. That is why make test has never needed a -configuration flag, and why Step 10’s archive comes out in Release without asking for it.

Step 2: Name the device to build for

A destination is the device a build is for. Pressing Run in Xcode uses the device chosen in the toolbar; xcodebuild needs it spelled out with -destination, and -showdestinations lists the ones that fit the scheme. The full list includes every simulator on your Mac, and your Mac itself with its hardware identifier, so this command keeps three lines, and sed removes the tabs xcodebuild indents them with:

xcodebuild -showdestinations -quiet -project TodoList/TodoList.xcodeproj -scheme TodoList | grep -E 'TodoList-iPhone|Any iOS' | sed 's/^[[:space:]]*//'
{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device }
{ platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Any iOS Simulator Device }
{ platform:iOS Simulator, arch:arm64, id:ADED6804-7A8D-4BCC-A43F-897446B5AA35, OS:27.0, name:TodoList-iPhone }

The Makefile names a destination in one of two ways:

  • A specific simulator, platform=iOS Simulator,name=TodoList-iPhone, for anything that installs or runs the app: build, run, and test.
  • A generic device, generic/platform=iOS, matching the Any iOS Device line. It compiles for real iPhones without needing one connected. Archives use it: an archive is for devices, not for a simulator, and the generic destination needs no device plugged in.

The simulator’s identifier is random and differs on your Mac.

Step 3: Read the build settings that apply

A build setting is one named value that controls the build, such as the app’s version number or the compiler’s optimization level. Each target in each configuration resolves several hundred of them. -showBuildSettings prints the resolved values; count them first:

xcodebuild -quiet -project TodoList/TodoList.xcodeproj -scheme TodoList -destination 'generic/platform=iOS' -showBuildSettings | grep -c ' = '
607

The exact count varies with the Xcode version and the Mac; another run during validation printed 609.

Each value comes from one of several layers, and a higher layer replaces a lower one:

  1. Xcode’s defaults for the platform and the SDK (the iOS frameworks and headers the build compiles against).
  2. The project’s settings, which apply to every target, plus any .xcconfig file the project uses as its base (Step 6 adds one).
  3. Each target’s own settings, plus any .xcconfig file the target uses.
  4. Settings given on the xcodebuild command line.

Both layers 2 and 3 are stored in project.pbxproj, the file inside TodoList.xcodeproj that records the project. Within each of them, a value typed into Xcode’s Build Settings editor (select the project or a target, then the Build Settings tab) replaces the same setting in that layer’s .xcconfig file. -showBuildSettings prints only the final value, so to see the layers work, override one from the command line:

xcodebuild -quiet -project TodoList/TodoList.xcodeproj -scheme TodoList -destination 'generic/platform=iOS' -showBuildSettings MARKETING_VERSION=9.9 | grep -o 'MARKETING_VERSION = .*'
MARKETING_VERSION = 9.9

MARKETING_VERSION is the version number users see, 1.0 in the project. The 9.9 from the command line wins for this one command and changes nothing on disk. The full output also contains paths with your user name in them, which is one reason the Makefile in Step 7 prints only the settings it names.

Step 4: Find where each build lands

Every build writes its intermediate files and its products into a DerivedData folder. Xcode puts one per project under your Library folder; the sed command removes the indentation and shortens your home folder to ~:

xcodebuild -quiet -project TodoList/TodoList.xcodeproj -scheme TodoList -destination 'generic/platform=iOS' -showBuildSettings | grep -E '^ *BUILD_DIR =' | sed -e 's/^ *//' -e "s#$HOME#~#"
BUILD_DIR = ~/Library/Developer/Xcode/DerivedData/TodoList-duysjjjqokrlcugjoxsntflvtdri/Build/Products

The suffix after TodoList- is derived from the project’s path, so it differs on your Mac. The Makefile passes -derivedDataPath build instead, so its builds land in todo-list/build, where make clean can delete them and .gitignore already ignores them. The two folders do not share builds: a build in Xcode leaves build/ untouched, so make build starts no further ahead, and a make build leaves Xcode’s folder untouched.

After the builds from the earlier articles, the build folder holds:

ls build build/Build/Products
build:
Build
CompilationCache.noindex
Index.noindex
info.plist
Logs
ModuleCache.noindex
SDKExplicitPrecompiledModules
SDKStatCaches.noindex
SourcePackages
TestResults

build/Build/Products:
Debug-iphonesimulator

Build/Products has one folder per configuration and platform, which is where make run finds the app to install. SourcePackages holds what Xcode resolved for packages, and Logs holds the build and test logs. The rest are caches and indexes Xcode manages itself, including info.plist, which is Xcode’s record of the folder, not the app’s Info.plist.

Step 5: Write the .xcconfig files

Settings typed into Xcode’s Build Settings editor live inside project.pbxproj, mixed in with the rest of the project and hard to review in a diff. An .xcconfig file holds the same NAME = value lines as plain text. The three files below go in their own folder next to the project, not inside TodoList/TodoList/: that folder is synchronized with the app target, so Xcode would copy any file in it into the app.

Create the file

mkdir -p Config
touch Config/Shared.xcconfig Config/Debug.xcconfig Config/Release.xcconfig

Add the code: Config/Shared.xcconfig

// Settings for every target in the TodoList project, in every configuration.
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES

Add the code: Config/Debug.xcconfig

#include "Shared.xcconfig"

// Debug builds show a different name on the home screen.
INFOPLIST_KEY_CFBundleDisplayName = To-Do Dev

Add the code: Config/Release.xcconfig

#include "Shared.xcconfig"

INFOPLIST_KEY_CFBundleDisplayName = To-Do

Detailed breakdown

  • Shared.xcconfig holds what every configuration shares. SWIFT_TREAT_WARNINGS_AS_ERRORS = YES makes any Swift compiler warning in the project’s targets fail the build, so warnings get fixed instead of piling up. It does not reach the TodoKit package, which takes its settings from its own manifest.
  • #include pulls one file into another. Debug and Release each include the shared file, then add their own lines. A setting defined in both places takes the value that comes last, here the configuration’s own.
  • INFOPLIST_KEY_CFBundleDisplayName is the name under the app’s icon. The app’s Info.plist is the file of keys and values iOS reads about the app, such as its name and version. This project generates it from build settings: a setting named INFOPLIST_KEY_ plus a supported key becomes that key. The project does not set a display name anywhere else, so these values take effect: without them, the home screen shows the target name, TodoList.
  • Comments start with //, the last piece of syntax these files use.

Step 6: Attach the files to the project in Xcode

Xcode reads an .xcconfig file only when the project refers to it and a configuration is based on it. You set both in Xcode. Open the project:

open TodoList/TodoList.xcodeproj

First add the files to the project without adding them to a target:

  1. Choose File → Add Files to “TodoList”…, open the todo-list/Config folder, select all three files, and click Add.
  2. In the options sheet, set Action to Reference files in place, and clear the TodoList checkbox under Targets so no target is ticked. Click Finish. A target would copy the files into the app.

Then base each configuration on its file:

  1. Click TodoList, the blue project icon at the top of the Project navigator, then the TodoList entry under PROJECT (not under TARGETS), and the Info tab.
  2. Under Configurations, expand Debug. The first TodoList row, with the blue project icon, is the project; set its menu to Debug.xcconfig.
  3. Expand Release and set its project row to Release.xcconfig.

Leave the target rows at None. The project row is layer 2 from Step 3, so its file applies to every target in that configuration.

Xcode records each choice as a baseConfigurationReference in project.pbxproj. grep -o prints just the matching part of each line, without the file’s indentation:

grep -o 'baseConfigurationReference.*' TodoList/TodoList.xcodeproj/project.pbxproj
baseConfigurationReference = 30BE926E308BE39600F0E3F8 /* Debug.xcconfig */;
baseConfigurationReference = 30BE926F308BE39600F0E3F8 /* Release.xcconfig */;

The long identifiers are Xcode’s object IDs for the two files and differ in your project.

Step 7: Extend the Makefile

The Makefile gains two make targets, settings (a short list of build settings) and archive, plus a CONFIG variable so one make target builds either configuration. Replace the whole Makefile.

Create the file

touch Makefile

Add the code: Makefile

PROJECT     := TodoList/TodoList.xcodeproj
PACKAGE     := TodoKit
SCHEME      := TodoList
BUNDLE_ID   := com.example.TodoList
SIM_NAME    := TodoList-iPhone
DEVICE_TYPE := iPhone 17
DERIVED     := build
# Debug or Release; override with `make build CONFIG=Release`.
CONFIG      := Debug
ifeq ($(filter $(CONFIG),Debug Release),)
$(error CONFIG must be Debug or Release, not "$(CONFIG)")
endif
APP         := $(DERIVED)/Build/Products/$(CONFIG)-iphonesimulator/TodoList.app
ARCHIVE     := $(DERIVED)/TodoList.xcarchive
SIM         := -destination 'platform=iOS Simulator,name=$(SIM_NAME)'
DEVICE      := -destination 'generic/platform=iOS'
# Keep only result lines from the test logs: Swift Testing (✔/✘),
# XCTest ("Test Case ... passed"), errors, and xcodebuild's final verdict.
RESULTS     := grep -E '(✔|✘) Test|Test Case .*(passed|failed)|error:|\*\* TEST'
# The build settings `make settings` prints, out of several hundred.
SETTINGS    := CONFIGURATION|INFOPLIST_KEY_CFBundleDisplayName|MARKETING_VERSION|DEBUG_INFORMATION_FORMAT|SWIFT_TREAT_WARNINGS_AS_ERRORS
XCODEBUILD  := xcodebuild -project $(PROJECT) -scheme $(SCHEME) -derivedDataPath $(DERIVED)
TEST_FLAGS  := -parallel-testing-enabled NO -collect-test-diagnostics never

# bash, so the test recipes can use `set -o pipefail` (see below).
SHELL         := /bin/bash
.DEFAULT_GOAL := help
.PHONY: help simulator settings build package-test test run screenshot archive clean delete-simulator

help: ## Show this help
	@echo "Usage: make <target> [CONFIG=Debug|Release]"
	@echo
	@grep -E '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*## "}; {printf "  %-17s %s\n", $$1, $$2}'

simulator: ## Create the TodoList-iPhone simulator if it is missing
	@xcrun simctl list devices available | grep -q "$(SIM_NAME) (" || \
		xcrun simctl create "$(SIM_NAME)" "$(DEVICE_TYPE)"

settings: ## Show the key build settings for CONFIG
	@set -o pipefail; \
		$(XCODEBUILD) $(DEVICE) -configuration $(CONFIG) -showBuildSettings 2>/dev/null | \
		grep -E '^ *($(SETTINGS)) =' | sed 's/^ *//'

build: simulator ## Build the app for the simulator in CONFIG
	$(XCODEBUILD) $(SIM) -configuration $(CONFIG) -quiet build

# pipefail makes `... | grep` fail when the command before it fails. It is set
# inside the recipe because macOS ships GNU Make 3.81, which ignores .SHELLFLAGS.
# SWT_SF_SYMBOLS_ENABLED=0 makes swift test print ✔/✘ instead of SF Symbols.
package-test: ## Run the TodoKit tests on the Mac (no simulator)
	set -o pipefail; SWT_SF_SYMBOLS_ENABLED=0 swift test --package-path $(PACKAGE) \
		--disable-xctest 2>&1 | $(RESULTS)

test: package-test simulator ## Run the package tests, then the UI test
	set -o pipefail; $(XCODEBUILD) $(SIM) $(TEST_FLAGS) test 2>&1 | $(RESULTS)

run: build ## Boot the simulator, install the CONFIG build, and launch it
	xcrun simctl bootstatus "$(SIM_NAME)" -b
	open "$$(xcode-select -p)/../Applications/DeviceHub.app"
	xcrun simctl install "$(SIM_NAME)" "$(APP)"
	xcrun simctl launch "$(SIM_NAME)" $(BUNDLE_ID)

screenshot: ## Save a screenshot of the simulator to screenshots/
	@mkdir -p screenshots
	xcrun simctl io "$(SIM_NAME)" screenshot screenshots/todolist.png

# An archive is a Release build for real devices. It is left unsigned, because
# signing needs a development team; see the article.
archive: ## Archive an unsigned Release build for devices into build/
	$(XCODEBUILD) $(DEVICE) -archivePath $(ARCHIVE) CODE_SIGNING_ALLOWED=NO -quiet archive
	@plutil -extract ApplicationProperties json -o - $(ARCHIVE)/Info.plist; echo

clean: ## Delete the build folders
	rm -rf $(DERIVED) $(PACKAGE)/.build

delete-simulator: ## Delete the TodoList-iPhone simulator
	xcrun simctl delete "$(SIM_NAME)"

Detailed breakdown

  • CONFIG := Debug sets the default. A variable given on the make command line, as in make build CONFIG=Release, replaces any assignment in the Makefile; a CONFIG variable in your shell’s environment does not, so it cannot change builds by accident. APP uses CONFIG to find the matching Release-iphonesimulator or Debug-iphonesimulator folder.

  • The ifeq block rejects any other value. xcodebuild does not reject an unknown configuration name. Given a misspelling such as Relase, -showBuildSettings quietly reports the Release values, and a build fails with the misleading Unable to resolve module dependency: 'TodoKit'. With the check, make settings CONFIG=Relase stops before running anything:

    Makefile:11: *** CONFIG must be Debug or Release, not "Relase".  Stop.
    
  • The destination moved out of XCODEBUILD. SIM and DEVICE hold the two destinations from Step 2, so archive can build for devices while the other make targets use the simulator.

  • settings prints five settings out of about six hundred. SETTINGS is a grep pattern of names, and sed removes the four-space indent xcodebuild puts before each setting. set -o pipefail keeps a failing xcodebuild a failure, as in the test recipes. The recipe uses the generic device destination, so it works without the simulator. Errors from xcodebuild go to /dev/null to keep the output short; if make settings prints only make: *** [settings] Error 1, run the same command without 2>/dev/null to see why.

  • test takes no CONFIG. The scheme’s Test action uses Debug and builds and installs the app itself, so CONFIG would have no effect there.

  • archive turns signing off. CODE_SIGNING_ALLOWED=NO is a build setting on the command line, layer 4 from Step 3. Step 10 explains why it is needed. The recipe then prints the archive’s application properties as JSON.

  • The help line names CONFIG, so the option shows up in plain make.

Check the help screen:

make
Usage: make <target> [CONFIG=Debug|Release]

  help              Show this help
  simulator         Create the TodoList-iPhone simulator if it is missing
  settings          Show the key build settings for CONFIG
  build             Build the app for the simulator in CONFIG
  package-test      Run the TodoKit tests on the Mac (no simulator)
  test              Run the package tests, then the UI test
  run               Boot the simulator, install the CONFIG build, and launch it
  screenshot        Save a screenshot of the simulator to screenshots/
  archive           Archive an unsigned Release build for devices into build/
  clean             Delete the build folders
  delete-simulator  Delete the TodoList-iPhone simulator

Step 8: Check where each setting came from

Before building anything, confirm the .xcconfig files took effect. Debug first:

make settings
CONFIGURATION = Debug
DEBUG_INFORMATION_FORMAT = dwarf
INFOPLIST_KEY_CFBundleDisplayName = To-Do Dev
MARKETING_VERSION = 1.0
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES

Then Release:

make settings CONFIG=Release
CONFIGURATION = Release
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
INFOPLIST_KEY_CFBundleDisplayName = To-Do
MARKETING_VERSION = 1.0
SWIFT_TREAT_WARNINGS_AS_ERRORS = YES

The display names and SWIFT_TREAT_WARNINGS_AS_ERRORS came from the new files, since nothing else in the project sets them. DEBUG_INFORMATION_FORMAT comes from the project’s own settings: Debug uses dwarf, debug information inside the build, and Release uses dwarf-with-dsym, which also writes a separate dSYM file of debug symbols that turns crash reports back into function names. Step 10’s archive includes one.

A setting in the file that does nothing

A value in the project’s base .xcconfig file sits below every value typed into the Build Settings editor, so it has no effect on a setting the project or a target already defines. Add a version number to the shared file and check:

echo 'MARKETING_VERSION = 2.0' >> Config/Shared.xcconfig
make settings | grep MARKETING_VERSION
MARKETING_VERSION = 1.0

The version is still 1.0, because the TodoList target sets MARKETING_VERSION itself (layer 3, stored in project.pbxproj). To manage a setting like this one from a file, you also delete its value from each target that sets it, here TodoList and TodoListUITests, in the Build Settings editor (select the row and press Delete), so the file’s value can show through. Take the line back out:

sed -i '' '/^MARKETING_VERSION/d' Config/Shared.xcconfig
make settings | grep MARKETING_VERSION
MARKETING_VERSION = 1.0

When a setting in an .xcconfig file seems to do nothing, make settings (or the full -showBuildSettings) shows whether a higher layer sets it.

Step 9: Build and run both configurations

The display names make the configuration visible on the simulator. Build and launch the Debug build:

make run
xcrun simctl launch "TodoList-iPhone" com.example.TodoList
com.example.TodoList: 40080

The excerpt shows the last two lines. Quit the app, then screenshot the home screen:

xcrun simctl terminate TodoList-iPhone com.example.TodoList
make screenshot
xcrun simctl io "TodoList-iPhone" screenshot screenshots/todolist.png
Detected file type from extension: PNG
Note: No display specified. Defaulting to display: 459A9BC3-6621-403B-817E-B45E64657398 (screenID: 1, name: LCD)

A last line, Wrote screenshot to:, gives the full path. The home screen in screenshots/todolist.png shows the app’s icon labeled To-Do Dev. The display identifier differs on your Mac. The TodoListUITests icon next to it is the runner app make test installed to drive the UI test.

Now build and launch Release:

make run CONFIG=Release
xcodebuild -project TodoList/TodoList.xcodeproj -scheme TodoList -derivedDataPath build -destination 'platform=iOS Simulator,name=TodoList-iPhone' -configuration Release -quiet build
xcrun simctl bootstatus "TodoList-iPhone" -b
Monitoring boot status for TodoList-iPhone (ADED6804-7A8D-4BCC-A43F-897446B5AA35).
Device already booted, nothing to do.

open "$(xcode-select -p)/../Applications/DeviceHub.app"
xcrun simctl install "TodoList-iPhone" "build/Build/Products/Release-iphonesimulator/TodoList.app"
xcrun simctl launch "TodoList-iPhone" com.example.TodoList
com.example.TodoList: 40308

The app opens with the same to-do list: both builds share the bundle identifier com.example.TodoList, the name iOS tells apps apart by, so installing Release replaced Debug. Check the name it shows by reading the built app’s Info.plist:

plutil -extract CFBundleDisplayName raw build/Build/Products/Release-iphonesimulator/TodoList.app/Info.plist
To-Do

Quit the app again and the home screen shows To-Do:

xcrun simctl terminate TodoList-iPhone com.example.TodoList

Step 10: Test, then archive a Release build

The new SWIFT_TREAT_WARNINGS_AS_ERRORS applies to the app and the UI-test target, so run the tests before archiving. They still pass:

make test
set -o pipefail; SWT_SF_SYMBOLS_ENABLED=0 swift test --package-path TodoKit \
		--disable-xctest 2>&1 | grep -E '(✔|✘) Test|Test Case .*(passed|failed)|error:|\*\* TEST'
✔ Test summaryCountsOnlyUnfinishedItems() passed after 0.010 seconds.
✔ Test addTodoIgnoresBlankTitles() passed after 0.010 seconds.
✔ Test addTodoStoresTheCleanedTitle() passed after 0.010 seconds.
✔ Test blankTitlesAreRejected(raw:) with 3 test cases passed after 0.010 seconds.
✔ Test titlesAreTrimmed(raw:expected:) with 3 test cases passed after 0.010 seconds.
✔ Test run with 5 tests in 1 suite passed after 0.011 seconds.
set -o pipefail; xcodebuild -project TodoList/TodoList.xcodeproj -scheme TodoList -derivedDataPath build -destination 'platform=iOS Simulator,name=TodoList-iPhone' -parallel-testing-enabled NO -collect-test-diagnostics never test 2>&1 | grep -E '(✔|✘) Test|Test Case .*(passed|failed)|error:|\*\* TEST'
Test Case '-[TodoListUITests.TodoListUITests testTodoIsSavedAcrossLaunches]' passed (15.304 seconds).
** TEST SUCCEEDED **

An archive is what Xcode’s Product → Archive produces: a Release build for devices, packaged with its dSYM files in an .xcarchive folder, ready to be signed and uploaded. Without CODE_SIGNING_ALLOWED=NO, archiving stops at signing. The error line starts with the path to the project file, followed by:

error: Signing for "TodoList" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'TodoList' from project 'TodoList')

A development team comes with an Apple Developer account, and this article does not assume one. With signing turned off, archiving succeeds:

make archive
xcodebuild -project TodoList/TodoList.xcodeproj -scheme TodoList -derivedDataPath build -destination 'generic/platform=iOS' -archivePath build/TodoList.xcarchive CODE_SIGNING_ALLOWED=NO -quiet archive
{"CFBundleShortVersionString":"1.0","CFBundleIdentifier":"com.example.TodoList","Team":"","ApplicationPath":"Applications\/TodoList.app","SigningIdentity":"","Architectures":["arm64"],"CFBundleVersion":"1"}

SigningIdentity and Team are empty because nothing was signed, and Architectures lists arm64, the processor architecture of current iPhones. CFBundleShortVersionString is MARKETING_VERSION from Step 3. Running make archive again replaces the archive without asking. Look inside:

find build/TodoList.xcarchive -maxdepth 3
plutil -extract CFBundleDisplayName raw build/TodoList.xcarchive/Products/Applications/TodoList.app/Info.plist
build/TodoList.xcarchive
build/TodoList.xcarchive/Products
build/TodoList.xcarchive/dSYMs
build/TodoList.xcarchive/Info.plist
build/TodoList.xcarchive/Products/Applications
build/TodoList.xcarchive/dSYMs/TodoList.app.dSYM
build/TodoList.xcarchive/Products/Applications/TodoList.app
build/TodoList.xcarchive/dSYMs/TodoList.app.dSYM/Contents
To-Do

The app inside carries the Release display name, and dSYMs holds the debug symbols that dwarf-with-dsym produced. The next step in a real release, xcodebuild -exportArchive, signs the app and packages it for the App Store or for testers. It fails on this archive with No Team Found in Archive, and it belongs with the articles that set up signing.

Troubleshooting

  • make settings prints only make: *** [settings] Error 1. xcodebuild failed, and its error went to /dev/null. Run the command from the recipe without 2>/dev/null.
  • CONFIG must be Debug or Release. The check is case-sensitive, although xcodebuild itself accepts release: use CONFIG=Release.
  • A setting in an .xcconfig file has no effect. A higher layer sets it. Check with make settings or the full -showBuildSettings, then delete the value from each target or project that sets it, in the Build Settings editor. See Step 8.
  • The display name is still TodoList. The configurations are not based on the files. Check Step 6: the file belongs on the project row, and the grep there should print two lines.
  • The .xcconfig files appear inside the built app. They were added to the TodoList target or placed inside TodoList/TodoList/. Remove them from the target and keep them in Config/.
  • Signing for "TodoList" requires a development team. The archive ran without CODE_SIGNING_ALLOWED=NO. Use make archive.

Recap

xcodebuild -list and -showdestinations name the pieces of a build that Xcode’s Run button chooses for you: the scheme, its targets and configurations, and the destination. -showBuildSettings shows the result of four layers of build settings, and a command-line override or a value that does nothing shows which layer applies. Builds land in DerivedData, in Xcode’s folder or in build/ for make. Three .xcconfig files now hold the settings this article added as reviewable text, two of them attached as the project’s base configurations, and the display name on the home screen shows which configuration is running. The Makefile builds, runs, and inspects either configuration with CONFIG, and archives an unsigned Release build with its debug symbols.

Next improvements:

  • Move existing settings out of project.pbxproj into the files, deleting each one in the Build Settings editor as you go.
  • Add a Config/Version.xcconfig for the version numbers once the target values are removed, so a release changes one file.
  • Share the TodoList scheme (Product → Scheme → Manage Schemes…, Shared) so it is saved in the project folder instead of generated.