클라우드 Mac에서 빌드가 통과하고 압축 파일도 정상적으로 풀리더라도, 수신 측에서는 여전히 ._Config.json, .DS_Store가 보이거나 업로드 단계에서 출처를 알 수 없는 확장 속성이 발견될 수 있습니다. 원인은 대개 컴파일러가 아니라 Finder 조작, 파일 시스템 간 복사, 그리고 아카이브 도구마다 다른 macOS 메타데이터 처리 방식에 있습니다. 해결책은 작업 공간 전체를 한 번에 거칠게 정리하는 것이 아니라, 배포 과정을 감사 가능한 스테이징, 검사, 패키징, 재검증의 네 단계로 나누는 것입니다.
배포 패키지에 포함할 수 있는 항목부터 정의하기
macOS 파일에는 이름과 내용 외에도 확장 속성, 리소스 포크, Finder 메타데이터가 포함될 수 있습니다. 로컬 파일 시스템에서는 문제가 없더라도 ZIP, 공유 드라이브 또는 객체 스토리지로 옮기면 AppleDouble 사이드카 파일로 변환될 수 있습니다.
패키징하면서 삭제하지 말고, 먼저 산출물 정책을 정의해야 합니다.
| 대상 | 기본 정책 | 이유 |
|---|---|---|
.DS_Store |
거부 | Finder의 디렉터리 보기 방식만 설명함 |
._* |
거부 | 리소스 포크가 파일 시스템 간 변환 과정에서 생성되는 경우가 많음 |
com.apple.quarantine |
검사 후 선별 처리 | 다운로드 도구나 브라우저에서 유입될 수 있음 |
com.apple.ResourceFork |
산출물 유형에 따라 판단 | 일반 구성 패키지에는 보통 필요 없지만 특정 파일은 의존할 수 있음 |
| 서명된 앱 번들 | 구조를 보존하고 재검증 | 무차별적인 정리는 서명 오류 위험을 높임 |
정리 대상은 소스 작업 공간이 아니라 별도의 스테이징 디렉터리여야 합니다. 그래야 정책이 잘못되더라도 캐시, 의존성 디렉터리 또는 다음 빌드에 필요한 입력을 변경하지 않습니다.
배포 파일에 서명된 앱이 포함되어 있다면 먼저 원본 산출물의 서명을 검증해야 합니다. 일반 문서, 구성 파일, 로그에는 더 엄격한 메타데이터 거부 정책을 적용할 수 있지만, 이 두 유형의 대상에 동일한 재귀 삭제 명령을 사용해서는 안 됩니다.
일회성 스테이징 디렉터리 만들기
스테이징 디렉터리는 현재 작업만 독점적으로 사용해야 하며, 작업이 끝날 때 삭제해야 합니다. 고정된 /tmp/release를 재사용하면 병렬 작업이 서로의 파일을 덮어쓸 수 있습니다.
#!/bin/bash
set -euo pipefail
SOURCE="${1:?source path required}"
OUTPUT="${2:?output path required}"
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/bamini-artifact.XXXXXX")"
UNPACK="$(mktemp -d "${TMPDIR:-/tmp}/bamini-unpack.XXXXXX")"
cleanup() {
rm -rf "$STAGE" "$UNPACK"
}
trap cleanup EXIT
/usr/bin/ditto --norsrc "$SOURCE" "$STAGE/payload"
find "$STAGE/payload" -type f -name '.DS_Store' -delete
find "$STAGE/payload" -type f -name '._*' -delete
mktemp를 사용하면 작업마다 서로 다른 디렉터리가 생성되고, trap은 성공, 실패, 중단을 포함한 모든 종료 경로에서 정리를 수행합니다. 여기서는 ditto --norsrc로 콘텐츠를 복사해 리소스 포크가 함께 복사되지 않도록 합니다. 다만 입력 디렉터리에 이미 실체화된 ._ 파일이 있을 수 있으므로 후속 검사는 계속 필요합니다.
워크플로에서 완전한 앱 번들을 배포해야 한다면, 먼저 복사 정책이 해당 앱에 필요한 콘텐츠를 보존하는지 검증해야 합니다. 더 안전한 방법은 앱 번들과 일반 첨부 파일을 별도로 스테이징한 뒤 각각의 정책을 적용하는 것입니다.
확장 속성을 전부 지우지 말고 검사하기
xattr -cr은 편리하지만 범위가 지나치게 넓습니다. 알려진 격리 속성뿐 아니라 아직 검사 규칙에 포함되지 않은 다른 속성까지 삭제합니다. 그 결과 파이프라인은 정상 상태로 바뀌지만 근본 원인은 가려집니다.
먼저 속성 이름을 출력한 뒤, 명시적으로 금지된 항목이 발견되면 작업을 중단하도록 구성할 수 있습니다.
ATTR_REPORT="$STAGE/xattr.txt"
if /usr/bin/xattr -lr "$STAGE/payload" >"$ATTR_REPORT" 2>&1; then
if grep -E 'com\.apple\.(quarantine|ResourceFork)' "$ATTR_REPORT"; then
echo "disallowed macOS metadata found" >&2
exit 41
fi
fi
if find "$STAGE/payload" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
grep -q .; then
echo "hidden metadata files found" >&2
exit 42
fi
이 검사는 “발견 즉시 실패”하는 방식을 사용하므로, 메타데이터가 어느 단계에서 유입되는지 먼저 파악할 때 적합합니다. 출처를 확인한 다음 다운로드, 복사 또는 생성 단계에서 해당 원인만 선별적으로 수정해야 합니다. 업무상 특정 확장 속성을 허용해야 한다면 전체 디렉터리를 통과시키지 말고, 허용 목록에 파일 범위와 속성 이름을 명시해야 합니다.
메타데이터가 유입된 단계 기록하기
소스 체크아웃 후, 컴파일 후, 스테이징 후에 각각 한 번씩 검사합니다. 이상이 처음 나타난 단계가 조사 범위의 경계입니다. 흔한 유입 경로로는 그래픽 인터페이스로 디렉터리 탐색, 비네이티브 파일 시스템에서 복사, 도구를 이용한 선다운로드 후 압축 해제 등이 있습니다. 최종 패키징 직전에만 검사하면 오염된 파일을 차단할 수는 있지만, 어느 단계가 원인인지는 확인할 수 없습니다.
패키징 후 다시 열어 검사하기
스테이징 디렉터리가 깨끗하더라도 아카이브 도구가 메타데이터를 다시 기록하지 않는다는 보장은 없습니다. ZIP을 생성한 후에는 멤버 이름을 검사하고, 새 디렉터리에 압축을 풀어 다시 검증해야 합니다.
rm -f "$OUTPUT"
/usr/bin/ditto -c -k --norsrc --keepParent \
"$STAGE/payload" "$OUTPUT"
/usr/bin/unzip -Z1 "$OUTPUT" >"$STAGE/members.txt"
if grep -E '(^|/)\.DS_Store$|(^|/)\._[^/]+$' "$STAGE/members.txt"; then
echo "archive contains forbidden metadata files" >&2
exit 43
fi
/usr/bin/ditto -x -k "$OUTPUT" "$UNPACK"
if find "$UNPACK" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
grep -q .; then
echo "extracted artifact failed metadata check" >&2
exit 44
fi
멤버 목록 검사는 빠르므로 첫 번째 검사 단계에 적합합니다. 압축 해제 후 재검증은 경로 변환과 압축 해제 동작까지 확인합니다. 업로드 시스템이 실제로 받는 것은 아카이브 파일이고, 사용자가 최종적으로 다루는 것은 압축 해제된 결과이므로 두 검사 모두 수행해야 합니다.
패키지에 서명된 앱이 포함되어 있다면 압축 해제된 .app을 찾아 다시 엄격하게 서명을 검증해야 합니다. 패키징 전 복사본만 검증하면 아카이브 매개변수로 인해 발생한 변경을 발견할 수 없습니다.
반복 가능한 배포 흐름에 검사 단계 통합하기
스크립트는 “입력 디렉터리, 출력 파일”이라는 두 개의 고정 매개변수를 받도록 구성하고, 작업마다 다음 증빙 자료를 생성하는 것이 좋습니다.
- 스테이징 전 산출물의 경로와 유형.
- 확장 속성 검사 결과.
- ZIP 멤버 목록.
- 압축 해제 후 숨김 파일 검사 결과.
- 앱 번들이 있을 때 수행한 2차 서명 검증 결과.
- 최종 아카이브의 SHA-256 요약값.
요약값은 shasum -a 256 artifact.zip으로 생성할 수 있습니다. 이 값만으로 두 ZIP의 콘텐츠가 의미상 완전히 동일하다는 것을 증명할 수는 없지만, 업로드, 다운로드, 인계 과정에서 바이트 단위의 변경이 없었는지는 확인할 수 있습니다.
BAMini의 클라우드 Mac에서 이러한 작업을 실행할 때는 먼저 콘솔에서 현재 선택할 수 있는 구성을 확인한 다음, 스크립트를 저장소에 넣고 CI가 호출하도록 해야 합니다. 특정 구성원이 수동 정리를 기억하는 방식에 의존해서는 안 됩니다. 검사에 실패하면 멤버 목록과 속성 보고서는 보관하고, 프로젝트 콘텐츠가 들어 있는 임시 디렉터리는 삭제합니다. 다음 작업에서는 반드시 새 스테이징 디렉터리를 만들어 이전 작업의 잔여물이 판단에 영향을 주지 않도록 해야 합니다.
최종 목표는 모든 확장 속성을 없애는 것이 아니라, 배포 패키지에 팀이 명시적으로 승인한 콘텐츠만 포함되도록 하는 것입니다. 스테이징 격리는 변경 범위를 통제하고, 단계별 검사는 유입 지점을 추적하며, 압축 해제 후 재검증은 실제 배포 결과를 확인합니다. 이 세 가지를 함께 적용해야 숨은 메타데이터가 서로 다른 복사 경로를 통해 반복적으로 나타나는 문제를 막을 수 있습니다.
자주 묻는 질문
전체 작업 디렉터리에 xattr -cr을 실행해도 되나요?
권장하지 않습니다. 필요한 리소스 포크까지 삭제하고 상위 단계의 문제를 숨길 수 있습니다. 별도 스테이징 디렉터리에서 허용 목록에 따라 필요한 속성만 제거해야 합니다.
.DS_Store를 지웠는데도 압축 파일에 ._ 파일이 남는 이유는 무엇인가요?
._ 파일은 AppleDouble 보조 파일이며 .DS_Store와 다릅니다. 패키징 도구에서 리소스 포크 복사를 끄고 생성된 압축 파일의 전체 항목을 다시 검사해야 합니다.
서명된 앱은 어느 시점에 검증해야 하나요?
원본 앱을 먼저 검증한 뒤 스테이징 디렉터리로 복사합니다. 패키징 후에는 새 디렉터리에 압축을 풀고 엄격한 서명 검증을 다시 실행해야 합니다.
다음 빌드를 클라우드 Mac mini에서 실행하세요
구성, 결제 주기와 노드 지역을 선택하고 전용 물리 컴퓨터에서 개발, 빌드와 자동화 작업을 실행하세요.