A build can finish successfully on a cloud Mac, only for scripts to lose their executable permissions, symbolic links to turn into regular files, or application bundle validation to fail after the artifact is downloaded to the delivery machine. These problems often originate not during compilation, but in the final compression and extraction step. In addition to contents and paths, macOS files can carry extended attributes, access control lists, resource forks, and symbolic link relationships. If the delivery process checks only that “the archive opens,” those changes can make it all the way to release.
First define what must be preserved
Do not compress the entire workspace directly. Start by creating a staging directory that contains only the deliverables, and explicitly define which metadata is part of the artifact contract.
Typical deliverables include application bundles, frameworks, dynamic libraries, command-line tools, configuration templates, and symbol files. Cache directories, temporary test files, version control directories, and local logs should not be included. A staging directory also prevents build tasks from continuing to modify the source directory while packaging is in progress.
set -euo pipefail
ROOT="$PWD"
SOURCE="$ROOT/build/Release"
STAGE="$ROOT/artifacts/stage"
ARCHIVE="$ROOT/artifacts/release.zip"
rm -rf "$STAGE"
mkdir -p "$STAGE"
ditto "$SOURCE/MyProduct.app" "$STAGE/MyProduct.app"
ditto "$SOURCE/MyFramework.framework" "$STAGE/MyFramework.framework"
The first inspection should record the number of regular files, directories, and symbolic links. Symbolic links are especially prone to being dereferenced when passed between tools, which can change the archive size or break relative paths.
find "$STAGE" -type f | wc -l
find "$STAGE" -type d | wc -l
find "$STAGE" -type l -print
Inspect macOS metadata before packaging
Use ls -le@ to inspect permissions, ACLs, and extended attributes, and use xattr to expand the attributes of important objects. Do not assume that every unfamiliar attribute is junk.
ls -le@ "$STAGE"
xattr -lr "$STAGE" > "$ROOT/artifacts/xattr-before.txt"
find "$STAGE" -type l -exec sh -c '
for item do
printf "%s -> %s\n" "$item" "$(readlink "$item")"
done
' sh {} + > "$ROOT/artifacts/symlinks-before.txt"
Do not treat
xattr -cras a universal fix. Recursive removal may temporarily hide some visible errors, but it also destroys diagnostic evidence. The correct sequence is to inventory the attributes, determine where they came from, and then remove only individual attributes that have been confirmed as unnecessary.
Also inspect the permission bits on executable files. A script being executable in the repository does not guarantee that it remains executable after being copied into the staging directory. Use find "$STAGE" -type f -perm -111 to establish a baseline. If a script must be executable, encode that requirement in the acceptance script instead of relying on someone to remember it.
Create a deliverable archive with ditto
ditto is included with macOS and is well suited to application bundles and framework directories. When creating the ZIP file, preserve resources and extended attributes, and retain the staging directory itself so the recipient gets a stable top-level structure.
rm -f "$ARCHIVE"
ditto -c -k \
--sequesterRsrc \
--keepParent \
"$STAGE" \
"$ARCHIVE"
shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
du -h "$ARCHIVE"
Keep both the archive filename and its internal top-level directory fixed. Do not put timestamps, machine names, or absolute paths to temporary workspaces in directory names, because downstream scripts would then have to guess the entry point. If the pipeline needs a unique identifier, store the commit number in the external artifact record instead of changing the archive layout.
For files from uncontrolled sources, inspect filenames for line breaks, invisible characters, and unusual Unicode combinations before they enter the staging directory. Ordinary spaces can be handled safely, but scripts must always double-quote variables to prevent the Shell from splitting paths.
Extract into a separate directory and compare
Do not extract over the original directory during acceptance testing, because old files can conceal missing ones. Always use a fresh directory and let ditto perform the extraction.
VERIFY="$ROOT/artifacts/verify"
rm -rf "$VERIFY"
mkdir -p "$VERIFY"
ditto -x -k "$ARCHIVE" "$VERIFY"
test -d "$VERIFY/stage/MyProduct.app"
test -d "$VERIFY/stage/MyFramework.framework"
find "$VERIFY/stage" -type l -exec sh -c '
for item do
printf "%s -> %s\n" "$item" "$(readlink "$item")"
done
' sh {} + > "$ROOT/artifacts/symlinks-after.txt"
diff -u \
"$ROOT/artifacts/symlinks-before.txt" \
"$ROOT/artifacts/symlinks-after.txt"
For file content verification, generate separate manifests for the directory before packaging and after extraction. Paths may contain spaces, so avoid loops that use unquoted values. The Python snippet below sorts entries by relative path and calculates SHA-256 for regular files.
from hashlib import sha256
from pathlib import Path
import sys
root = Path(sys.argv[1])
for path in sorted(root.rglob("*"), key=lambda p: str(p.relative_to(root))):
if path.is_file() and not path.is_symlink():
digest = sha256(path.read_bytes()).hexdigest()
print(digest, path.relative_to(root))
Run it separately against stage and verify/stage, then compare the output with diff. This detects content changes, missing files, and unexpected additions at the same time.
Turn structure and signature checks into a release gate
If the artifact contains a signed application bundle or framework, verify its signature again after extraction. Checking only before compression is not enough, because the archiving tool may introduce the problem.
codesign --verify --deep --strict \
"$VERIFY/stage/MyProduct.app"
codesign -d --verbose=4 \
"$VERIFY/stage/MyProduct.app" 2>&1 |
sed -n '1,20p'
--deep is appropriate for a final integrity check, but it does not replace layer-by-layer diagnosis. If verification fails, first determine whether the failure is in the main application, an embedded framework, or a helper executable. Then run codesign --verify --strict --verbose=4 against the specific path. Also compare the xattr output from before and after packaging to confirm whether the archive step caused the change.
Recommended failure conditions are: a different top-level directory, mismatched file manifests, changed symbolic link targets, missing required executable bits, unexpected changes to critical extended attributes, or signature verification failure. Any one of these should stop the release instead of triggering another compression attempt in the hope that it works.
Common pitfalls and delivery checklist
The most common mistake is mixing multiple tools: a script first creates a regular ZIP file, then the upload program automatically extracts and recompresses it. The longer the chain, the harder it becomes to track metadata semantics. Designate one packaging tool and send the original archive directly to artifact storage.
The second mistake is packaging while the build is still running. If the compiler is still writing files, the manifest and archive may represent different points in time. Wait for the build process to exit successfully, copy the results into a read-only staging directory, and only then begin archiving.
The third mistake is testing only on the same machine. At a minimum, delete the previous verification directory, extract the archive again, and run checks using paths inside the extracted package. When possible, have an independent job run the same scripts as well. Long-running pipelines on BerryVM can keep the inventory files, archive hash, and acceptance logs together as the delivery record for a single run, but those logs must not contain credentials or environment variables that have not been redacted.
Keep the final checklist short: the staging directory is frozen, symbolic links are recorded, extended attributes are inventoried, the archive hash has been generated, clean extraction is complete, file manifests match, executable bits are as expected, and all applicable signature checks pass. With these controls in place, packaging is no longer an incidental step after the build. It becomes a repeatable engineering gate that fails accurately when something changes.
Frequently asked questions
Why is zip -r not always suitable for macOS build artifacts?
A generic ZIP workflow can omit resource forks, extended attributes, ACLs or symlink semantics. App bundles, frameworks and tool directories are safer when archived with ditto and verified after extraction.
Should a pipeline run xattr -cr before every package step?
No. Inventory the attributes first and remove only entries that have been reviewed and found unnecessary. Recursive deletion can erase metadata needed for delivery or diagnosis.
Is matching the archive SHA-256 enough to approve an artifact?
No. It proves only that the archive file is unchanged. Extract it into a separate directory and verify the tree, symlinks, attributes and code signature where applicable.
Move your next development task to a cloud Mac
Choose Berry M4 or Berry M4 Pro, review the five nodes, rental period, and add-ons, then place your order.