The same Xcode project may build successfully in a long-lived development directory, then report missing files, unavailable scripts, or empty configuration values after being copied to a fresh VPSGit cloud Mac workspace. Do not start by transferring old caches. The real test is whether a build can complete using only the specified commit, an explicitly declared toolchain, and parameters injected by the pipeline.
What to isolate during clean checkout validation
Deleting DerivedData eliminates only one class of cache. It does not prove that the project has no machine-local dependencies. A long-lived workspace commonly accumulates uncommitted files, user-directory configuration, globally installed tools, Shell initialization scripts, and absolute paths outside the repository.
At minimum, isolate these five layers:
- Use an independent clone instead of cleaning the original directory.
- Create temporary
HOMEandTMPDIRdirectories for the job. - Assign a job-specific directory to DerivedData.
- Pin
DEVELOPER_DIRto prevent the default Xcode version from drifting. - Pass only allowlisted environment variables to the build process.
The purpose of clean checkout validation is not to “clean one more time” before building. It is to prove that the build cannot silently use any state that is absent from version control, dependency manifests, or pipeline parameters.
During the initial investigation, keep a control case: the original workspace builds successfully while the isolated workspace fails. Both builds must use the same commit, Scheme, Configuration, and Destination so that the difference in behavior has diagnostic value.
Create a disposable isolated workspace
The script below creates a clone from an existing repository without using local hard links, checks out the specified commit, and places the user directory, temporary directory, and build cache in a disposable location. The project name and Scheme are supplied as arguments so that environment-specific details are not hard-coded into the script.
#!/bin/bash
set -euo pipefail
SOURCE_REPO="${1:?source repository required}"
REVISION="${2:?revision required}"
PROJECT="${3:?project path required}"
SCHEME="${4:?scheme required}"
ROOT="$(mktemp -d "${TMPDIR:-/tmp}/clean-checkout.XXXXXX")"
trap 'rm -rf "$ROOT"' EXIT
git clone --no-local --no-checkout "$SOURCE_REPO" "$ROOT/repo"
git -C "$ROOT/repo" checkout --detach "$REVISION"
git -C "$ROOT/repo" submodule update --init --recursive
mkdir -p "$ROOT/home" "$ROOT/tmp" "$ROOT/DerivedData"
export HOME="$ROOT/home"
export TMPDIR="$ROOT/tmp"
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
cd "$ROOT/repo"
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration Debug \
-derivedDataPath "$ROOT/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
build | tee "$ROOT/build.log"
CODE_SIGNING_ALLOWED=NO is appropriate only for validating compilation stages that do not require signing. If the target must be archived or signed, CI should inject the required materials securely. Do not reference files in a personal user directory just to make the script pass.
Expose missing dependencies early
After cloning, run git status --porcelain first. It should produce no output. Then verify the state of all submodules and confirm that large-file objects are complete. If the project includes code generation, distinguish clearly between two types of artifacts: the generator source and version must be pinned, while generated output must either be committed according to team policy or produced reliably during the build.
Do not add ~/bin, /usr/local/bin, or a team member’s desktop directory as a fallback path. Supporting tools should live in a repository tools directory, be declared in a version-constrained package manifest, or be installed at a fixed version by the node initialization process.
Trace hidden inputs from failure logs
After an isolated build fails, find the first real error rather than the summary at the end of the log. Common signals can be grouped by input type:
| Failure signal | Common cause | Remediation |
|---|---|---|
No such file or directory |
Uncommitted file or absolute path | Add the file to the repository and use a repository-relative path |
command not found |
Dependency on an interactive Shell or globally installed tool | Declare the tool version and set PATH explicitly |
| Empty configuration value | Parameter exists only in local configuration | Inject it through CI and fail immediately when it is missing |
| Module is visible in the old directory | A shared cache masks a missing dependency | Fix the dependency declaration instead of copying the old cache |
| Script permission error | The executable bit was not included in the commit | Correct the file mode and commit the change again |
Use the following commands to confirm whether the current commit actually contains the expected file and permissions:
git ls-tree -r HEAD -- path/to/file
git status --short
git diff --summary HEAD
If the failing path points to /Users/某个名字/, an absolute path has usually been recorded in project settings, a script, or a generated file. Fix the source configuration that produced the path instead of creating a matching directory on the new node.
Restrict the environment without creating false failures
Running env -i directly can reveal environment dependencies quickly, but it may also remove the PATH, locale settings, and temporary directory required by the build. A more reliable approach is to first record the variables the build actually reads, then create an allowlist.
Keep HOME, TMPDIR, PATH, DEVELOPER_DIR, LANG, and any application-specific parameters explicitly injected by the pipeline. Add a required-value check for every custom variable, for example:
: "${API_BASE_URL:?API_BASE_URL must be provided by CI}"
: "${BUILD_FLAVOR:?BUILD_FLAVOR must be provided by CI}"
This makes missing configuration fail at the script entry point instead of producing an ambiguous error late in compilation. For sensitive values, check only whether they exist; do not echo them into logs. Build scripts should also avoid reading interactive Shell configuration because non-interactive jobs may never load those files.
Add validation to the regular pipeline
A fully independent clone increases checkout time and disk usage, so it does not need to block every small commit. Split validation into two levels: regular jobs can reuse a workspace for fast feedback, while clean checkout jobs run when changes are merged into the main branch, dependencies or Xcode change, and before a release.
Validation should pass only when all of the following conditions are met:
- The specified commit can be checked out completely in an independent clone.
- The working tree has no unexpected modifications before or after the build.
- The build does not read from the original workspace or personal user directories.
- Every supporting tool has a traceable version source.
- The expected artifacts are still produced after DerivedData is cleared.
- Failure logs and the environment summary provide enough information to reproduce the issue without exposing sensitive values.
When clean checkout validation fails but a regular build succeeds, treat it as a dependency declaration defect. Fix the input boundaries before discussing cache restoration. Keeping this validation in place turns new-node handoffs, parallel capacity expansion, and toolchain upgrades from “try another machine and hope” into a reproducible engineering process.
Frequently asked questions
Why is running git clean not enough?
git clean only removes untracked files from the current worktree. It does not isolate HOME settings, global tools, shared caches, Keychain contents, or absolute paths outside the repository.
What should I inspect first after a clean build fails?
Compare the failing command’s input paths and environment variables first, then verify that submodules, large files, generated sources, configuration files, and helper tools are explicitly declared.
Should clean-checkout validation run on every commit?
Not necessarily. Keep a short smoke check in the regular pipeline and run full isolation before releases, after toolchain or dependency changes, and on important merges to the main branch.
Configure a cloud Mac for your next build or inference task
Choose from three Apple Silicon configurations, six nodes, and fixed rental terms. Actual availability is shown in real time in the console.