A Swift Package may work perfectly in local previews yet show blank icons, fallback copy, or JSON read failures once it is added to an archive job on a cloud Mac. The hardest part is often not a compiler error, but the fact that the pipeline still reports success. SwiftPM handles Bundle.module, but it does not determine whether the team has declared every resource, whether lookup names match, or whether the resources made it into the final app bundle. The solution is to validate resources as part of the build artifact instead of waiting for testers to discover missing content after opening a screen.
Identify the three places where resources can go missing
Before troubleshooting, determine which layer contains the failure. At the first layer, the file exists in the source directory, but the target in Package.swift does not declare the corresponding resource. At the second, the resource has been built into a .bundle, but the calling code uses the main application bundle. At the third, the file is present in the app bundle, but its filename casing, subdirectory, or localization directory does not match the runtime lookup.
Keep a minimal reproduction entry point that loads one image, one JSON file, and one localized string. If all three resource types fail, check bundle selection first. If only one file fails, start with its resource rule and path.
“The build passed” proves only that the compiler accepted the current inputs. It does not prove that the files required at runtime are present in the deliverable.
In Package.swift, explicitly choose either .process or .copy. Images, resource directories, and localized content generally belong under .process. Use .copy only for files whose original directory structure or raw bytes must be preserved. Do not copy the entire repository directory into the resource bundle at once. Doing so can include test samples, temporary output, and even configuration that should never be shipped.
Define the resource contract with a manifest
Resource validation cannot rely on file counts alone. Adding a resource while deleting or renaming an old one may leave the total count unchanged. A more reliable approach is to maintain ci/expected-resources.txt in the repository, with one path per line relative to the root of the resource bundle:
Assets.car
Defaults/config.json
en.lproj/Localizable.strings
zh-Hans.lproj/Localizable.strings
The manifest should contain only files that must be available at runtime. On-demand downloads, test fixtures, and development preview resources should not be included. A resource rename must appear in code review together with the corresponding manifest change so reviewers can see exactly how the delivery contract has changed.
Run the check in the opposite direction as well. Files that exist in the source tree but are not declared should produce warnings, while files required by the manifest but absent from the build output should fail the job immediately. The former helps clean up the repository; the latter protects the deliverable.
Inspect the built app bundle directly
Give every job its own DerivedData directory so stale bundles left by another parallel job cannot make the check pass incorrectly. The following script uses a simulator build as an example. In a real pipeline, replace the scheme and update the bundle-name pattern to match the project’s actual module name.
set -euo pipefail
DERIVED="${RUNNER_TEMP:-/tmp}/resource-check-${BUILD_ID:-local}"
rm -rf "$DERIVED"
xcodebuild \
-scheme ExampleApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
build
APP=$(find "$DERIVED/Build/Products" -type d -name 'ExampleApp.app' -print -quit)
test -n "$APP"
COUNT=$(find "$APP" -type d -name '*FeatureKit*.bundle' | wc -l | tr -d ' ')
test "$COUNT" = "1"
BUNDLE=$(find "$APP" -type d -name '*FeatureKit*.bundle' -print -quit)
while IFS= read -r item; do
test -z "$item" && continue
test -e "$BUNDLE/$item" || {
printf 'missing resource: %s\n' "$item" >&2
exit 1
}
done < ci/expected-resources.txt
The script deliberately asserts that exactly one bundle exists before selecting it. Taking the first match immediately could allow a stale artifact, test bundle, or identically named module to hide the problem. The check must also target the bundle inside the .app, not an intermediate DerivedData directory, because only the former represents the final embedded result.
Turn common pitfalls into CI gates
Different resource types require different validation methods. A simple test -e is not enough for all of them.
| Resource type | Minimum validation | Common failure |
|---|---|---|
| JSON | The file exists and can be parsed | An empty or malformed file was copied |
| Localized strings | The target language directory exists and required keys are readable | The language directory is misnamed or the app falls back to default copy |
| Images and colors | The compiled resource artifact exists and loads on a smoke-test screen | Name casing does not match or the main bundle is used by mistake |
| Template files | Validate the hash or critical fields | A generation step overwrote the repository version |
Common macOS work volumes are case-insensitive, so a mismatch such as IconDark versus icondark may be hidden on a developer machine. Do not depend on the filesystem to tolerate the mistake. Extract paths from the manifest and compare them character for character, with case sensitivity, against the actual paths. Run a parser against JSON, use plutil -lint for property lists, and check at least the required languages and critical keys in string directories.
If the Package uses a build tool plugin for generation, first confirm that generation runs before resource validation and restrict the output directory to a temporary path owned by that job. When multiple jobs share the same generated directory, one job may read files produced by another branch, creating the most difficult kind of false success to reproduce.
Preserve evidence and keep jobs reproducible
Do not upload the entire DerivedData directory when a job fails. It is large, noisy, and may contain path information that should not be retained long term. A more useful evidence bundle includes the resource manifest, the actual bundle file tree, the end of the build log, parser errors, and the current commit identifier. Generate the file tree like this:
find "$BUNDLE" -print \
| sed "s#^$BUNDLE/##" \
| LC_ALL=C sort \
> resource-bundle-tree.txt
Before archiving logs and file trees, remove access tokens, user directories, and temporary credentials. Delete the isolated DerivedData and temporary generation directories when the job finishes so every subsequent run starts from an empty directory. Parallel runners on DPLYMAC should follow the same rule: jobs may reuse download caches, but they must not reuse unidentified build output.
The final gate should answer three questions: what the declarations require, what is actually present in the final app bundle, and whether runtime code can read it using the real name. Once each layer produces an inspectable result, missing SwiftPM resources stop being intermittent UI failures and become ordinary build errors that can be diagnosed at commit time.
Frequently asked questions
Why can a SwiftPM package compile successfully but fail to load a resource at runtime?
Compilation does not guarantee that the runtime name, path casing, or final bundle location is correct. Inspect the generated .app and its resource bundle directly.
Should validation inspect the source tree or only the built application?
Inspect both. Source checks reveal files omitted from the package declaration, while built-product checks prove that resources were processed and embedded in the deliverable.
Run validated workflows on dedicated physical nodes
Choose M4 or M4 Pro, your target node, and billing cycle for the task, then review the configuration and add-ons before placing your order.