SwiftPM 6.4 build artifact paths are no longer safe to infer from a hard-coded .build hierarchy: run swift build --show-bin-path with the same build parameters, then update cache and collection logic around that returned path. Use the native build system only as a temporary diagnostic fallback, and keep production on the new path only after a dual-track validation.

This is for you if you maintain Shell, Makefile, Fastlane, or CI scripts and now see “build succeeded, artifact not found.” It also applies to Swift Package authors, plugin maintainers, test engineers, and DevOps teams managing remote Mac nodes, caches, and rollback rules.

First, classify the failure before changing the toolchain

The most useful symptom is not the missing file. It is the sequence of events around it:

  1. The build command exits successfully.
  2. A later step tries to locate a binary, test result, coverage file, or archive.
  3. The upload or packaging step reports that the file does not exist.
That sequence points first to artifact discovery, not compilation. If the build itself returns a failure status, investigate source errors, dependency resolution, plugin execution, or toolchain compatibility before changing path logic.

SwiftPM 6.4 documentation identifies Swift Build as the default build system and documents output-location changes. The official Swift Build migration documentation also points users toward querying the output directory instead of treating internal directory names as a permanent interface.

Record these values from the failed job before rerunning it:

  • The complete build command.
  • The complete artifact-discovery command.
  • The working directory printed by the runner.
  • The selected toolchain and xcode-select state.
  • The build configuration, architecture, and destination.
  • The exact exit status of every command.
  • The path returned by swift build --show-bin-path.
  • The path that the upload step actually searched.
  • The cache key and whether the workspace was clean.
A log saying only “file not found” is not enough evidence. You need to know whether the producer wrote the file somewhere else, whether the consumer searched from the wrong directory, or whether a stale cache supplied an artifact from another task.

Check the path contract used by each responsibility

The same output path can be consumed differently by different parts of a Swift repository. Treat each consumer as a separate contract instead of applying one global replacement.

Script maintainers: remove assumptions from collection commands

Search Shell scripts, Makefiles, Fastlane lanes, and custom release helpers for patterns such as:

.build/debug/MyBinary
.build/release/MyBinary
.build/arm64-apple-macosx/release/MyBinary

The problem is not that every reference is guaranteed to fail. The problem is that these paths are inferred from an internal layout rather than obtained from the build command that created the product.

A safer pattern is:

set -eu

ROOT_DIR="${CI_PROJECT_DIR:-$PWD}"
CONFIGURATION="release"
ARCHITECTURE="arm64"
PACKAGE_PATH="$ROOT_DIR/<package-path>"
PRODUCT_NAME="<product-name>"

cd "$PACKAGE_PATH"

swift build \
  -c "$CONFIGURATION" \
  --arch "$ARCHITECTURE"

BIN_PATH="$(
  swift build \
    -c "$CONFIGURATION" \
    --arch "$ARCHITECTURE" \
    --show-bin-path
)"

BIN_PATH="$(printf '%s' "$BIN_PATH" | tr -d '\n')"
PRODUCT_PATH="$BIN_PATH/$PRODUCT_NAME"

test -d "$BIN_PATH"
test -f "$PRODUCT_PATH"

mkdir -p "$ROOT_DIR/<artifact-directory>"
cp "$PRODUCT_PATH" "$ROOT_DIR/<artifact-directory>/"

The important rule is parameter symmetry. The build and the path query must use the same package location, configuration, architecture, and destination. If your actual job uses a destination or another supported build option, repeat it in the query rather than asking for a default path.

The official package manager repository is the appropriate source for supported command behavior. Do not copy path logic from an old log, a local machine, or an internal .build directory that happened to exist before the migration.

Package authors: inspect plugins and resource-producing targets

A package plugin can create a second path problem even after the main binary lookup is fixed. Review build tool plugins, command plugins, binary targets, resource processing, and script plugins for code that concatenates .build subdirectories manually.

For each plugin, document:

  • Which input files it consumes.
  • Which output files it creates.
  • Whether the output is declared to the build system.
  • Whether the output is copied into a stable release directory.
  • Which command produces the log used for diagnosis.
  • Whether the plugin assumes one configuration or one architecture.
A plugin that writes to a private temporary location without declaring its output can appear to work on a warm workspace and fail on a clean remote Mac. That is not necessarily a permissions issue. It may be an undeclared dependency or an output that is not part of the build graph.

The official helper implementation used by the Swift project is useful for studying how a larger project separates build arguments from output handling. Use it as an implementation reference, not as proof that your package has the same directory structure.

Compare the two build systems without treating either layout as permanent

Swift Build and the native build system should be compared by observable deliverables, not by whether their .build or derived-data directories look alike. The official Swift Build repository describes the separate build-system work, while the default-build-system change discussion explains why an upgrade can expose old assumptions in CI.

<
Validation areaSwift Build path strategyNative build path strategyPass condition
Binary discoveryQuery swift build --show-bin-path with matching parametersUse the native system’s supported result or archive interfaceThe collector reads the producer’s reported location
Test resultsRead the configured test report or log sourceRead the native test result bundle or configured reportFailed tests remain identifiable
Cache identityInclude toolchain, build system, configuration, architecture, and lockfile stateKeep a separate cache namespaceNo cross-system artifact reuse
Plugin outputsValidate declared inputs and outputsValidate the native phase or script outputClean builds reproduce required files
Release packagingCopy from the discovered path into a stable staging directoryPackage from the native archive or export resultThe final package is verified independently
A reported [Swift Build output-directory issue](https://github.com/swiftlang/swift-build/issues/1363) is a reason to preserve evidence and check known issues. It is not a reason to declare every remote failure a platform defect. First prove that the build parameters, workspace, and consumer path match.

If Swift Build fails while the native system succeeds, keep the smallest reproducible package, the full command, the toolchain selection, and the output logs. Do not silently mask the failure by switching production back to native. A fallback can help isolate the boundary, but it cannot replace release acceptance.

Rebuild test collection around evidence, not one guessed directory

Test jobs often fail after the binary path is repaired because test launchers and report collectors have their own assumptions.

Separate these checks:

  • swift test exit status.
  • Test execution logs.
  • Test result or report location.
  • Coverage output.
  • Failed-test identification.
  • Artifact upload status.
A file-existence check alone is weak. A report may exist while the test command failed, or a test command may pass while the collector uploads an old report from a warm workspace.

For a test job, make the producer and consumer visible:

set -eu

PACKAGE_PATH="<package-path>"
TEST_CONFIGURATION="debug"
TEST_ARCHITECTURE="arm64"

cd "$PACKAGE_PATH"

swift test \
  -c "$TEST_CONFIGURATION" \
  --arch "$TEST_ARCHITECTURE" \
  2>&1 | tee "<test-log-path>"

TEST_STATUS="${PIPESTATUS[0]}"
test "$TEST_STATUS" -eq 0

If the test command has a project-specific report option or a separate coverage export step, record its documented output rather than inventing a path from .build. Keep a native comparison job during migration, using the same commit and equivalent test scope. This lets you distinguish a changed report location from a changed test runner or a real test failure.

The Swift Build issue tracker entry for output differences should be attached to your internal incident record when the observed behavior matches it. Mark it as an external known issue, not as a substitute for your own command and environment evidence.

Rebuild cache identity on the remote Mac

A cache that predates the build-system change can make a corrected script look unreliable. The cache key must describe every input that can alter either the product or its location.

Include at least these dimensions in the cache identity:

  • SwiftPM and Swift toolchain version.
  • Selected build system.
  • Build configuration.
  • Target architecture.
  • Package dependency lockfile.
  • Relevant package manifest state.
  • Plugin source and generated-input state.
  • Runner image or macOS environment identifier.
  • Workspace layout, when the path is embedded in generated files.
Keep Swift Build and native caches separate. Never restore a native cache into a Swift Build job simply because both jobs use the same commit. A successful cache restore proves only that files were found in the cache; it does not prove that they belong to the current producer.

On a remote Mac, also inspect:

  • The account running SSH, VNC, or the CI agent.
  • The absolute workspace path.
  • File ownership and writable directories.
  • The active developer directory from xcode-select.
  • Cleanup behavior between jobs.
  • Whether a node restart removes temporary state.
  • Whether the agent starts before the expected login environment is available.
Do not begin by granting broad permissions. If the same command works under one account and fails under another, compare environment variables, working directories, ownership, and toolchain selection first.

If your team needs a disposable environment for this validation, you can review MACGPU’s remote Mac environment as one option for isolating cold-cache, restart, and toolchain checks without changing your primary workstation.

Use this acceptance checklist before production cutover

Run the checklist against the same commit and record the command output as build evidence.

  • [ ] Start from a clean workspace with no restored build directory.
  • [ ] Print the working directory before package commands run.
  • [ ] Print the selected developer directory and toolchain identity.
  • [ ] Record the exact SwiftPM 6.4 build command.
  • [ ] Run swift build --show-bin-path with identical configuration, architecture, and destination arguments.
  • [ ] Verify that the returned directory exists before appending a product name.
  • [ ] Confirm that the expected binary is produced by the current job, not copied from a previous workspace.
  • [ ] Run tests and preserve the test command’s exit status separately from report collection.
  • [ ] Validate coverage, logs, and test-result sources independently.
  • [ ] Run package plugins from a clean workspace and inspect declared outputs.
  • [ ] Use separate cache namespaces for Swift Build and native builds.
  • [ ] Repeat the job with a cold cache.
  • [ ] Repeat the job after restarting the remote Mac node.
  • [ ] Compare Swift Build and native results on the same commit.
  • [ ] Verify signing, packaging, checksum, and upload from the final staging directory.
  • [ ] Define the exact condition that permits production cutover or triggers rollback.
Score the result by risk rather than by a single green job:
  • Green: path discovery, tests, cache isolation, restart recovery, and release verification all pass on both tracks.
  • Amber: the build and upload pass, but restart, cold-cache, plugin, or native comparison evidence is missing. Keep the change in a trial lane.
  • Red: the build exits successfully but the path query disagrees with the collector, or the same commit produces unexplained differences between nodes. Stop the rollout.
A native pass with a Swift Build failure should produce a diagnostic package, not an automatic production fallback. The fallback is justified only when the release owner has documented the affected behavior, preserved the reproduction, and set a trigger for retesting.

Decide whether to stay, roll back, or isolate

The official Swift Evolution status and the Apple Developer Swift 6.4 materials should be checked before you label a toolchain state stable. As of August 27, 2026, SwiftPM 6.4 documentation confirms the default Swift Build transition and the output-location guidance, while Swift 6.4 is not marked there as an independently stable release. Xcode 27 status must be verified against its current release notes rather than inferred from media reports.

Choose one of these paths:

  • Continue with Swift Build when discovery uses the supported query, clean and warm caches agree, plugins declare outputs correctly, and release artifacts pass verification.
  • Temporarily use native when you need a controlled comparison to isolate a Swift Build behavior difference, but keep the failing Swift Build reproduction attached to the change.
  • Isolate the migration when production nodes cannot be restarted safely, cache namespaces cannot be separated, or the release process cannot verify test and package outputs independently.
Set a re-entry condition before rollback. Examples include a confirmed toolchain release, an official fix for the matching issue, or a completed remote-node validation run. Reverting without such a condition usually turns a path migration into an undocumented permanent fork.

FAQ

Why can’t my SwiftPM 6.4 job find a binary inside .build?

SwiftPM 6.4 uses Swift Build as the default build system, and its artifact layout does not preserve every old .build directory assumption. A successful compiler command only proves that the build completed. It does not prove that a later upload step queried the correct directory. Replace hard-coded paths with swift build --show-bin-path and use the same configuration, architecture, and destination arguments for both commands.

How should CI discover the output directory with show-bin-path?

Run swift build with --show-bin-path in the same workspace and with the same build parameters used by the real build. Capture the output, remove the trailing newline, verify that the returned directory exists, and then copy or archive the expected file from that location. Do not query a default configuration while building another configuration, because the result can describe a different output tree.

How do Swift Build and the native build system differ for artifact collection?

Swift Build and the native build system can expose different output locations and different assumptions about test or package products. Treat their directory layouts as implementation details rather than a stable CI API. During migration, run both systems against the same commit, record commands and outputs, and compare the actual deliverables. Use native only as a diagnostic fallback unless the release process has completed full dual-track validation.

What should I do when local Swift builds pass but remote Mac CI cannot upload artifacts?

First separate compilation from collection: inspect the build exit status, working directory, toolchain selection, and the path returned on the remote node. Then check cache identity, execution user, workspace cleanup, and package-plugin output declarations. A remote Mac may expose environment drift that a local machine hides. Reproduce with a cold cache and after a node restart before changing permissions or reverting the toolchain.

SwiftPM 6.4 build artifact paths should be treated as queried build outputs, not as a directory tree that your CI is entitled to predict. After replacing hard-coded .build references, the remaining risk usually sits in cache identity, plugin declarations, test-report collection, or node drift.

If your existing Mac is unsuitable for a disruptive upgrade, a remote Mac can provide an isolated place to run cold-cache, restart, and dual-toolchain acceptance jobs. That approach avoids three common weaknesses of a fixed local machine: it may be occupied during long CI checks, its cache state may be difficult to reproduce, and a failed toolchain change can interfere with daily development. For a disposable or temporary validation node, you can compare available remote Mac CI options from MACGPU against buying and maintaining a dedicated Mac mini. Renting is less suitable for a permanent, heavy workload or a workflow that requires direct physical hardware interfaces, but it is often the cleaner choice for migration testing and short-lived CI capacity.