Highlight
Xcode 13 adds multi-platform framework targets, build rules, script input and output dependencies, xcfilelist and xcconfig syntax capabilities, allowing complex projects to use fewer targets to manage cross-platform builds and reduce duplication of work.
Core Content
After a cross-platform app has been developed for a long time, the project files will become difficult to maintain.
Fruta is a sample project that builds macOS, iOS, and watchOS simultaneously. It has three framework targets, one for each platform. They contain the same set of shared code, differing only in a macOS-specific file.
The problem with this structure is straightforward. Every time the build setting is changed, three targets must be synchronized. Every time you add a source file, make sure that all three Compile Sources build phases are added. Miss it once and a platform will fail at build time.
The first step given by Xcode 13 is to combine multiple platform frameworks into a multi-platform framework target. When developers set Supported Platforms to Any Platform, Xcode will automatically turn on Allow Multiplatform Builds. This target will be built once for each supported platform as needed.
Platform differences are still preserved. Fruta’sIngredient+macOS.swiftIt should only participate in the macOS build, so set the platform filter for this file in Compile Sources and check only macOS.
When the project becomes larger, there will also be problems with the build speed. In the past, many teams would stuff custom scripts into the Run Script phase. Scripts process all files at once, making it difficult for Xcode to know which inputs affect which outputs, and making it difficult to execute in parallel.
The second focus of this session is to move the work that can be separated into build rules. each.recipeThe file is generated independently.compiledrecipedocument. The build system can split tasks by input files and schedule them based on dependencies.
The remaining work that cannot be split will continue to be placed in the script phase. For example, merge multiple description files into one archive file. The key is to declare input file list and output file. Xcode knows which files the script reads and which files it writes to ensure the correct order and avoid conservative serialization.
The last problem comes from build settings. A common approach for complex projects is to click a lot of configurations in the Xcode UI. Over time, it becomes difficult to check who is covered. Xcode’s Levels view can display default values, project xcconfig, project settings, target xcconfig, target settings, and final resolved values..xcconfigFiles put configurations into text files to facilitate reuse, review, and combination.
Detailed Content
Multi-platform framework target
(01:10) Xcode 13 supports multiplatform framework target. Fruta originally had three framework targets: macOS, iOS, and watchOS. In the speech, I changed the Supported Platforms of the macOS framework target to Any Platform, and Xcode automatically set Allow Multiplatform Builds to Yes.
Supported Platforms = Any Platform
Allow Multiplatform Builds = Yes
Key points:
Supported Platforms = Any Platform: Let the same framework target support multiple Apple platforms. -Allow Multiplatform Builds = Yes: Notifies the build system to build this target as needed for each supported platform.- This setup reduces duplication of build phases and build settings.
(03:06) Platform-specific source code is processed through platform filter. Fruta’sIngredient+macOS.swiftOnly compiles in macOS builds.
Compile Sources:
Ingredient+macOS.swift
Filters: macOS
Key points:
Compile Sources: Control which source files participate in the compilation of the current target. -Ingredient+macOS.swift: macOS-specific files in the example. -Filters: macOS: Only let this file go into the macOS build.
Scheme build order and implicit dependencies
(04:59) In Xcode’s scheme build options, Apple recommends using Dependency Order. It builds targets in parallel according to the dependency graph. Manual Order has been deprecated, and the talk clearly states that it will slow down the build and may cause cycle errors when the scheme order is inconsistent with project dependencies.
Scheme > Edit Scheme > Build:
Build Order = Dependency Order
Find Implicit Dependencies = On
Key points:
Build Order = Dependency Order: Let Xcode arrange the build order according to the dependency graph. -Find Implicit Dependencies = On: Let Xcode automatically infer target dependencies based on linker flags and linked libraries.- This combination is suitable for cross-project dependency scenarios, because some dependencies cannot be directly added with explicit target dependency.
Use build rule to process independent input files in parallel
(06:29) Fruta’s Process Recipes script originally processed multiple recipe files in sequence. The calculations for each recipe are independent of each other, so Lecture moved it to the build rule.
File Pattern: *.recipe
Output Files:
$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).compiledrecipe
Key points:
File Pattern: *.recipe: Matches input files to be processed by this rule. -$(DERIVED_FILE_DIR): Apple recommends writing generated files to a directory managed by the build system. -$(INPUT_FILE_BASE): Take the base name of the current input file. -.compiledrecipe: Each input file corresponds to an output file.
(08:22) The build rule script processes one input file at a time. The script in the lecture passes the current input path and the first output path to the external script.
"$SRCROOT/Scripts/gen-code.sh" "$SCRIPT_INPUT_FILE" "$SCRIPT_OUTPUT_FILE_0"
Key points:
"$SRCROOT/Scripts/gen-code.sh": Call an external script in the project, and the script content is left outside the project file to facilitate source code management. -"$SCRIPT_INPUT_FILE": The absolute path of the input file being processed by the current rule. -"$SCRIPT_OUTPUT_FILE_0": The first output path in the Output Files table.- Double quotes: handle spaces and special characters in paths.
(08:45) The build rule will run once per architecture by default. For example, if the Mac App target has arm64 and x86_64, multiplied by four inputs, it will be executed eight times. Fruta’s output has nothing to do with the CPU architecture, so Run once per architecture is cancelled.
Build Rule:
Run once per architecture = Off
Key points:
Run once per architecture = Off: Prevent the same architecture-independent output from being generated repeatedly.- Applicable scenarios: Generate text, resource indexes, configuration files and other products that have nothing to do with CPU architecture.
- If the output is an architecture-dependent artifact such as object code, the default behavior should be retained.
Declare input and output for script phase
(10:15) Some jobs require reading all input at once. Fruta merges multiple description files into one file. This kind of work is suitable to continue in the script phase.
# Package up the recipes.
echo "packaging..."
for i in $(seq 0 $(expr ${SCRIPT_INPUT_FILE_LIST_COUNT} - 1)) ; do
infile_="SCRIPT_INPUT_FILE_LIST_$i"
eval infile=\$$infile_
while IFS= read -r file; do
cat "$file" >> "$SCRIPT_OUTPUT_FILE_0"
done < "$infile"
done
Key points:
SCRIPT_INPUT_FILE_LIST_COUNT: The number of input file lists. -SCRIPT_INPUT_FILE_LIST_$i: Get the i-th input file list variable name by index. -eval infile=\$$infile_: Parse the variable name into a real file list path. -while IFS= read -r file: Read the file path in xcfilelist line by line. -cat "$file" >> "$SCRIPT_OUTPUT_FILE_0": Append the contents of each input file to the first output file.
(11:34) When there are many input files, use build phase file list (.xcfilelist)manage. It has one path per line and also supports#Notes at the beginning.
$(SRCROOT)/Recipes/Instructions/berry-blue.md
$(SRCROOT)/Recipes/Instructions/carrot-chops.md
$(SRCROOT)/Recipes/Instructions/hulking-lemonade.md
$(SRCROOT)/Recipes/Instructions/kiwi-cutie.md
$(SRCROOT)/Recipes/Instructions/lemonberry.md
$(SRCROOT)/Recipes/Instructions/love-you-berry-much.md
$(SRCROOT)/Recipes/Instructions/mango-jambo.md
$(SRCROOT)/Recipes/Instructions/one-in-a-melon.md
$(SRCROOT)/Recipes/Instructions/papas-papaya.md
$(SRCROOT)/Recipes/Instructions/pina-y-coco.md
Key points:
$(SRCROOT): Project source code root directory.- Each line: a script phase input file.
-
.xcfilelist: Move large amounts of input from project files to text files to reduce project file conflicts.
(11:57) The script phase needs to reference this file list and declare the output path.
Input File Lists:
$(SRCROOT)/FileList.xcfilelist
Output Files:
$(PROJECT_TEMP_DIR)/instructions.mdarchive
Key points:
Input File Lists: Tells Xcode which input lists the script will read. -$(SRCROOT)/FileList.xcfilelist: Path to the file list in the lecture. -Output Files: Tells Xcode which files the script will write out. -$(PROJECT_TEMP_DIR)/instructions.mdarchive: The merged output file path.
(12:50) The script phase and build rule receive a set of environment variables. The script should read the input and output paths parsed by Xcode through these variables.
SCRIPT_INPUT_FILE_COUNT
SCRIPT_INPUT_FILE_n
SCRIPT_INPUT_FILE_LIST_COUNT
SCRIPT_INPUT_FILE_LIST_n
SCRIPT_OUTPUT_FILE_COUNT
SCRIPT_OUTPUT_FILE_n
SCRIPT_OUTPUT_FILE_LIST_COUNT
SCRIPT_OUTPUT_FILE_LIST_n
Key points:
SCRIPT_INPUT_FILE_COUNT:The number of paths in the Input Files table. -SCRIPT_INPUT_FILE_n: The absolute path of the nth input file, n starts from 0. -SCRIPT_INPUT_FILE_LIST_n: The nth parsed input file list path. -SCRIPT_OUTPUT_FILE_n: The absolute path of the nth output file. -SCRIPT_OUTPUT_FILE_LIST_n: The nth parsed output file list path.
Use aggregate target to avoid duplication of work
(13:15) Multi-platform targets will be built for multiple platforms. Fruta’s SmoothieKit will be built once for iOS and once for watchOS. The original script phase attempts to write the same output path in both builds. Xcode requires that only one task in the entire build can produce output for a given path, so the build fails.
Aggregate target: Resources
Run Script: package recipes
SmoothieKit target:
Target Dependencies: Resources
Key points:
Aggregate target: Resources: Host platform-independent scripting work. -Run Script: package recipes: Move the scripts, inputs and outputs in the original framework target to the aggregate target. -Target Dependencies: Resources: Let SmoothieKit depend on this aggregate target.- Result: The script is executed only once and both iOS and watchOS variants are built in the correct order.
Build settings hierarchy and xcconfig syntax
(16:46) The Levels view of Build Settings displays multiple configuration levels. The order given in the talk includes SDK default value, project level configuration settings file, project level settings, target configuration settings file, target level settings, and finally resolved value.
Default value
Project-level .xcconfig
Project-level settings
Target-level .xcconfig
Target-level settings
Resolved value
Key points:
Default value: The default value provided by the current SDK. -Project-level .xcconfig: Project-level configuration file value. -Project-level settings: Project-level settings in the project file. -Target-level .xcconfig: Target-level profile value. -Target-level settings: Target-level settings in the project file. -Resolved value: The final value after Xcode parsing.
(18:17).xcconfigThe basic form of is name, assignment operator and value.
MY_BUILD_SETTING_NAME = "A build setting value"
Key points:
MY_BUILD_SETTING_NAME:build setting name. -=: Assignment operator. -"A build setting value": Set value.
(18:30).xcconfigSupports conditional syntax. The talk lists configuration, architecture, and SDK conditions, and SDK conditions support wildcard matching.
MY_BUILD_SETTING_NAME = "A build setting value"
MY_BUILD_SETTING_NAME[config=Debug] = -debug_flag
MY_BUILD_SETTING_NAME[arch=arm64] = -arm64_only
MY_BUILD_SETTING_NAME[sdk=iphone*] = -ios_only
Key points:
- First line: default value.
-
[config=Debug]: Only takes effect in Debug configuration. -[arch=arm64]: Only effective in arm64 architecture. -[sdk=iphone*]: Match iPhone SDK,*Used for wildcarding.
(19:50) The build setting can be combined with other build settings. Evaluation happens from the inside out.
IS_BUILD_SETTING_ENABLED = NO
MY_BUILD_SETTING_NO = -use_this_one
MY_BUILD_SETTING_YES = -use_this_instead
MY_BUILD_SETTING = $(MY_BUILD_SETTING_$(IS_BUILD_SETTING_ENABLED))
Key points:
IS_BUILD_SETTING_ENABLED = NO: Control setting value. -MY_BUILD_SETTING_NO: The control value isNOthe actual settings used. -MY_BUILD_SETTING_YES: The control value isYESthe actual settings used. -$(MY_BUILD_SETTING_$(IS_BUILD_SETTING_ENABLED)): First find the internalNO, and then analyzeMY_BUILD_SETTING_NO。
(21:08) Build setting evaluation also supports path operators.
$(MY_PATH:dir)
$(MY_PATH:file)
$(MY_PATH:base)
$(MY_PATH:suffix)
$(MY_PATH:standardizepath)
Key points:
:dir: Get the directory. -:file: Get the file name. -:base: Get the base name without extension. -:suffix: Get the extension. -:standardizepath: Standardized path.
(21:21) Each path operator has a corresponding replacement form, and the default operator provides a default value for empty values.
$(MY_PATH:dir=/tmp)
$(MY_PATH:file=/better.swift)
$(MY_PATH:base=another)
$(MY_PATH:suffix=m)
$(MY_PATH:default=YES)
Key points:
:dir=/tmp: Replace the directory part. -:file=/better.swift: Replace the file name part. -:base=another:Replace base name.:suffix=m: Replace the extension. -:default=YES: Used when the original value is emptyYES。
(21:36).xcconfigOther files can be included. required include requires the file to exist. If it cannot be found, a compilation error will occur. optional include is only loaded if the file exists.
#include "common.xcconfig"
#include? "ci.xcconfig"
Key points:
#include "common.xcconfig": Must be included, if the file is missing, it will fail. -#include? "ci.xcconfig": Optional inclusion, will not fail if the file does not exist.- Paths are resolved relative to the location of the Xcode project file.
(22:20) The talk gives a CI scenario. The development machine wants the Swift compiler to warn about time-consuming expressions faster, and the CI machine is slower, so the threshold is raised. The method is to optionally include ci.xcconfig in common.xcconfig, and then use the default operator to set the default value.
// common.xcconfig
#include? "ci.xcconfig"
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -warn-long-expression-type-checking=$(MAX_EXPRESSION_TIME:default=200)
// ci.xcconfig
MAX_EXPRESSION_TIME = 500
Key points:
#include? "ci.xcconfig": Load overlay files only on CI machines. -OTHER_SWIFT_FLAGS = $(inherited) ...: Keep existing Swift compilation parameters and add new parameters. -$(MAX_EXPRESSION_TIME:default=200): Use 200 when there is no local setting. -MAX_EXPRESSION_TIME = 500:CI machines override defaults via separate files.
Core Takeaways
-
What to do: Merge the shared frameworks of iOS, macOS, and watchOS into a multi-platform target. Why it’s worth doing: Xcode 13’s multiplatform framework target can unify build phases and build settings to reduce synchronization errors. How to get started: Select an existing framework target, set Supported Platforms to Any Platform, and use platform filter to process platform-specific files.
-
What to do: Split the resource code generation script from Run Script phase into build rule. Why it’s worth doing: When each input file generates output independently, the build rule allows Xcode to schedule in parallel and determine whether to rerun based on input and output dependencies. How to start: Add in Build Rules
*.recipeIn this type of file mode, the output is written to$(DERIVED_FILE_DIR), the script readsSCRIPT_INPUT_FILEand writeSCRIPT_OUTPUT_FILE_0。 -
What to do: Complete the input file list and output files for all custom scripts. Why it’s worth doing: Once Xcode knows which files the script reads and writes, it can arrange them in the correct order and reduce conservative serialization. How to get started: Create
.xcfilelist, reference it in the Input File Lists of the Run Script phase, and write the script product path in the Output Files. -
What to do: Move platform-independent packaging scripts to the aggregate target. Why it’s worth doing: Multi-platform target will be built for multiple platforms, and the same output path can only be generated by one task. The aggregate target allows shared work to be executed only once. How to start: Create an aggregate target like Resources, move the script, input, and output there, and then let the framework target add Target Dependency.
-
What to do: Use
.xcconfigManage compilation parameters for Debug, local and CI. Why it’s worth doing: Once configuration is in a text file, it can be reused, reviewed, and combined by environment. How to start: Put the public settings incommon.xcconfig,use#include? "ci.xcconfig"To load CI coverage values, use$(MAX_EXPRESSION_TIME:default=200)Provide local defaults.
Related Sessions
- Meet Xcode Cloud — Learn how Xcode Cloud connects build, test, and distribution to Apple’s CI/CD services.
- Explore Xcode Cloud workflows — Learn to use workflows to automate analysis, testing, archiving, and distribution.
- Customize your advanced Xcode Cloud workflows — In-depth customization of Xcode Cloud’s scripting, source code management, and service integration.
- Detect bugs early with the static analyzer — Use the Xcode static analyzer to find code issues before it runs.
- Diagnose unreliable code with test repetitions — Use repeated test runs to locate sporadic failures and unstable code.
Comments
GitHub Issues · utterances