Oakmini Engineering Notes

Audit build path bugs on a case-sensitive APFS volume

DevOps & CI/CD ·~5 min read

Audit build path bugs on a case-sensitive APFS volume

The same repository may build successfully on a developer machine but fail in a remote build job with Module not found, or produce a package with a missing asset. Before repeatedly clearing caches, check the filesystem. The default filesystem configuration commonly used on macOS is case-insensitive, so Config.json and config.json may resolve to the same path. Hidden defects surface only when a script, dependency, or deployment target enforces exact case matching. The safest way to investigate is not to modify the system volume, but to mount an isolated case-sensitive APFS volume on a cloud Mac.

Determine whether the failure is related to path case

Start by finding paths in the failure logs instead of looking only at the final exit code. Prioritize the following symptoms:

Symptom Possible cause Verification
Imports work locally, but the build job cannot find a module The import path does not match the actual filename Use git ls-files to verify the full path
Only one of two committed assets remains The names differ only by letter case Clone the repository again on a case-sensitive volume
The first build after cleanup fails Stale artifacts were masking an incorrect reference Delete the output directory and run a full build
Files are overwritten during packaging A script normalizes filenames Inspect copy and rename steps

Also run diskutil info / and record the current system volume’s File System Personality. This step is only for confirming the environment. Do not attempt to convert the active system volume.

Path issues must be reproduced in a clean workspace. Copying old caches to the test volume can hide the original defect again.

Create an isolated APFS test volume

A sparse image grows as data is written, making it suitable for temporary testing. The commands below create a test volume with an 80GB maximum size. If the project has large dependencies, allocate enough capacity for the repository, dependencies, and peak build output.

mkdir -p "$HOME/apfs-lab"
hdiutil create \
  -size 80g \
  -type SPARSEBUNDLE \
  -fs "Case-sensitive APFS" \
  -volname BuildCase \
  "$HOME/apfs-lab/BuildCase.sparsebundle"

hdiutil attach "$HOME/apfs-lab/BuildCase.sparsebundle"
diskutil info "/Volumes/BuildCase"

Verify that the output explicitly identifies the volume as case-sensitive and that the mount point is /Volumes/BuildCase. Do not store access credentials, signing material, or long-lived data on this temporary volume.

Clone the repository again on the new volume

Do not drag and copy the existing workspace directly, because conflicting files may already have been lost. Clone the repository again from the authoritative remote and create a separate build directory:

mkdir -p /Volumes/BuildCase/work
cd /Volumes/BuildCase/work
git clone "$REPOSITORY_URL" project
cd project
git status --short

git status should produce no output at this point. If cloning already reports that a destination path exists, the repository tree likely contains paths that collide when case is folded.

Audit Git filenames and script references

The following script reads only Git-tracked paths, normalizes them to a canonical Unicode form, and groups them after case folding. It does not modify any files:

from collections import defaultdict
import subprocess
import unicodedata

raw = subprocess.check_output(
    ["git", "ls-files", "-z"],
    text=True
)
groups = defaultdict(list)

for path in raw.split(""):
    if not path:
        continue
    key = unicodedata.normalize("NFC", path).casefold()
    groups[key].append(path)

found = False
for paths in groups.values():
    if len(paths) > 1:
        found = True
        print("COLLISION")
        for path in paths:
            print(f"  {path}")

raise SystemExit(1 if found else 0)

Save it as tools/check_path_case.py, then run python3 tools/check_path_case.py. If it exits with status 1, standardize the affected names and commit the changes with git mv. When changing only letter case, rename the file to an intermediate name first and then to the intended name so that the original workspace does not ignore the change:

git mv Sources/config.json Sources/config.tmp
git mv Sources/config.tmp Sources/Config.json

A repository without filename collisions can still contain incorrect references. Continue searching build scripts, asset manifests, project configuration, and test fixtures for outdated paths. Pay particular attention to discrepancies between generated files and handwritten configuration.

Add the audit to a reproducible build gate

The test job should verify the mount point first, run the path audit next, and finally build from an empty output directory. This prevents files from being written back to the system volume when the test volume is not mounted.

set -euo pipefail

test -d /Volumes/BuildCase
cd /Volumes/BuildCase/work/project
python3 tools/check_path_case.py

rm -rf .build-output
mkdir .build-output
./scripts/build.sh "$PWD/.build-output"

If the project does not have a single build entry point, first wrap the existing commands in scripts/build.sh and pass the output directory explicitly. Dependency caches may be mounted separately, but disable old caches for the initial verification. Restore them one at a time after the build passes to identify which layer introduces the incorrect path.

Common pitfalls and final checks

Before completing the investigation, confirm each of the following:

  • The repository was cloned again on the test volume rather than copied from an old directory.
  • Filename fixes were made with git mv, and the commit shows explicit renames.
  • Scripts do not force paths to all lowercase or all uppercase.
  • Asset-copy commands fail when a destination name already exists instead of silently overwriting it.
  • Build artifacts, dependency directories, and logs all reside on the expected mount point.
  • Both a clean build and an incremental build have been run, with identical results.

After validation, export any required logs before detaching the test volume:

hdiutil detach "/Volumes/BuildCase"

Delete BuildCase.sparsebundle only after confirming that none of its data needs to be retained. For a long-term quality gate, keep the volume-creation script rather than the test data, and check the volume format, available capacity, and mount path at the start of every job.

Frequently asked questions

Should I convert the cloud Mac system volume to case-sensitive APFS?

No. Create a separate APFS sparse image, mount it for the test workspace, and detach it after validation. This keeps the system volume unchanged.

Why can Git miss a filename case collision in the working tree?

Git records path spelling, but a case-insensitive working tree can map differently cased names to one file. Reclone on the test volume and compare tracked paths after case folding.

Need a dedicated Mac mini physical node?

View available configurations, nodes, and fixed billing cycles, and put the article’s steps into practice on a cloud Mac environment built for ongoing use.

Rent a Mac mini now