클라우드 Mac 빌드 산출물 메타데이터 보존 패키징

CI/CD ·약 10분 읽기

클라우드 Mac 빌드 산출물 메타데이터 보존 패키징

클라우드 Mac에서 빌드는 정상적으로 끝났지만 산출물을 배포 시스템에 내려받은 뒤 스크립트가 실행되지 않거나, 심볼릭 링크가 일반 파일로 바뀌거나, 앱 번들 검증이 실패하는 경우가 있습니다. 이런 문제는 대개 컴파일 단계가 아니라 마지막 압축 및 압축 해제 과정에서 발생합니다. macOS 파일에는 내용과 경로뿐 아니라 확장 속성, 액세스 제어 목록, 리소스 포크, 심볼릭 링크 관계가 포함될 수 있습니다. 배포 과정에서 단순히 “압축 파일이 열리는지”만 확인하면 이러한 변경 사항이 릴리스 단계까지 그대로 넘어갑니다.

먼저 보존해야 할 대상을 정의하기

작업 공간 전체를 바로 압축하지 마십시오. 먼저 실제 배포할 내용만 포함하는 스테이징 디렉터리를 만들고, 어떤 메타데이터가 산출물 계약에 포함되는지 명확히 정의해야 합니다.

일반적인 대상에는 앱 번들, 프레임워크, 동적 라이브러리, 명령줄 도구, 구성 템플릿, 심볼 파일이 포함됩니다. 캐시 디렉터리, 테스트 임시 파일, 버전 관리 디렉터리, 로컬 로그는 포함하지 않아야 합니다. 스테이징 디렉터리를 사용하면 패키징 중에 빌드 작업이 원본 디렉터리를 계속 변경하는 문제도 방지할 수 있습니다.

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"

첫 번째 검사에서는 일반 파일, 디렉터리, 심볼릭 링크의 개수를 기록해야 합니다. 특히 심볼릭 링크는 여러 도구를 거치는 과정에서 대상 파일로 풀리기 쉬우며, 그 결과 아카이브 크기가 달라지거나 상대 경로가 무효화될 수 있습니다.

find "$STAGE" -type f | wc -l
find "$STAGE" -type d | wc -l
find "$STAGE" -type l -print

패키징 전에 macOS 메타데이터 점검하기

ls -le@로 권한, ACL, 확장 속성을 확인하고, xattr로 주요 객체의 속성 내용을 펼쳐서 확인합니다. 익숙하지 않은 속성을 모두 불필요한 데이터로 간주해서는 안 됩니다.

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"

xattr -cr을 범용 해결책으로 사용하지 마십시오. 재귀적으로 속성을 제거하면 일부 표면적인 오류가 잠시 사라질 수 있지만, 원인 분석에 필요한 정보까지 함께 삭제됩니다. 올바른 순서는 먼저 속성을 점검하고 출처를 판단한 다음, 필요 없다고 확인된 개별 속성만 최소 범위로 처리하는 것입니다.

실행 파일의 권한 비트도 확인해야 합니다. 저장소에서 실행 가능했던 스크립트가 스테이징 디렉터리로 복사된 뒤에도 같은 상태라는 보장은 없습니다. find "$STAGE" -type f -perm -111을 사용해 기준 상태를 기록할 수 있습니다. 특정 스크립트에 실행 권한이 반드시 필요하다면 사람의 기억에 의존하지 말고 해당 요구 사항을 검수 스크립트에 포함해야 합니다.

ditto로 배포용 압축 파일 만들기

ditto는 macOS에 기본으로 포함된 도구로, 앱 번들과 프레임워크 디렉터리를 처리하는 데 적합합니다. ZIP을 만들 때 리소스와 확장 속성을 보존하고 스테이징 디렉터리 계층까지 유지하면, 수신 측에서 항상 일정한 최상위 구조를 얻을 수 있습니다.

rm -f "$ARCHIVE"
ditto -c -k \
  --sequesterRsrc \
  --keepParent \
  "$STAGE" \
  "$ARCHIVE"

shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
du -h "$ARCHIVE"

아카이브 파일명과 내부 최상위 디렉터리 이름은 고정해야 합니다. 디렉터리 이름에 타임스탬프, 머신 이름, 임시 작업 공간의 절대 경로를 넣으면 다운스트림 스크립트가 진입점을 추측해야 합니다. 파이프라인에 고유 식별자가 필요하다면 패키지 내부 구조를 바꾸지 말고 외부 산출물 레코드에 커밋 번호를 기록하십시오.

출처를 통제할 수 없는 파일은 스테이징 디렉터리에 넣기 전에 파일명에 줄바꿈 문자, 보이지 않는 문자, 비정상적인 Unicode 조합이 있는지도 확인해야 합니다. 일반 공백은 안전하게 처리할 수 있지만, Shell이 경로를 분리하지 않도록 스크립트에서는 항상 변수를 큰따옴표로 감싸야 합니다.

별도 디렉터리에 압축을 풀고 비교하기

검수할 때 원본 디렉터리 위에 덮어쓰는 방식으로 압축을 풀어서는 안 됩니다. 누락된 파일이 기존 파일에 가려질 수 있기 때문입니다. 매번 완전히 새로운 디렉터리를 사용하고 ditto로 압축을 해제하십시오.

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"

파일 내용은 패키징 전 디렉터리와 압축 해제 후 디렉터리에서 각각 목록을 생성해 검증해야 합니다. 경로에 공백이 포함될 수 있으므로 따옴표로 보호하지 않은 반복문을 사용하지 마십시오. 다음 Python 코드는 상대 경로를 기준으로 정렬하고 일반 파일의 SHA-256을 계산합니다.

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))

이 코드를 stage와 verify/stage에 각각 실행한 뒤 diff로 출력을 비교합니다. 이렇게 하면 내용 변경, 파일 누락, 예기치 않은 파일 추가를 동시에 발견할 수 있습니다.

구조 및 서명 검사를 배포 게이트로 만들기

산출물에 서명된 앱 번들이나 프레임워크가 포함되어 있다면 압축을 푼 뒤 다시 서명을 검증해야 합니다. 아카이브 도구가 문제를 일으킬 수 있으므로 압축 전에 한 번만 검증해서는 충분하지 않습니다.

codesign --verify --deep --strict \
  "$VERIFY/stage/MyProduct.app"

codesign -d --verbose=4 \
  "$VERIFY/stage/MyProduct.app" 2>&1 |
  sed -n '1,20p'

--deep은 최종 무결성 검사에 적합하지만 계층별 원인 분석을 대체할 수는 없습니다. 검증에 실패하면 먼저 주 앱, 포함된 프레임워크, 보조 실행 파일 중 어느 위치에서 문제가 발생했는지 확인한 뒤, 해당 경로에 codesign --verify --strict --verbose=4를 실행해야 합니다. 패키징 전후의 xattr 출력도 함께 비교해 변경이 아카이브 단계에서 발생했는지 확인하십시오.

검수 실패 조건은 최상위 디렉터리 불일치, 파일 목록 차이, 심볼릭 링크 대상 변경, 필수 실행 비트 손실, 주요 확장 속성 이상 또는 서명 검증 실패로 고정하는 것이 좋습니다. 이 중 하나라도 발생하면 다시 압축해 운에 맡기지 말고 릴리스를 중단해야 합니다.

자주 발생하는 실수와 배포 체크리스트

가장 흔한 실수는 여러 도구를 혼용하는 것입니다. 먼저 스크립트에서 일반 ZIP을 만든 뒤 업로드 프로그램이 자동으로 압축을 풀고 다시 압축하는 식입니다. 처리 단계가 길어질수록 메타데이터의 의미를 추적하기 어려워집니다. 패키징 도구를 하나로 지정하고 원본 압축 파일을 산출물 저장소에 직접 전달해야 합니다.

두 번째 실수는 빌드와 패키징을 동시에 진행하는 것입니다. 컴파일러가 파일을 계속 쓰는 동안 목록과 압축 파일이 서로 다른 시점의 상태를 반영할 수 있습니다. 올바른 방법은 빌드 프로세스가 성공적으로 종료될 때까지 기다린 다음, 결과를 읽기 전용 스테이징 디렉터리에 복사하고 아카이브를 시작하는 것입니다.

세 번째 실수는 같은 머신에서만 테스트하는 것입니다. 최소한 기존 검수 디렉터리를 삭제하고 새로 압축을 푼 뒤, 패키지 내부 경로를 기준으로 검사를 실행해야 합니다. 가능하다면 별도의 작업에서 동일한 스크립트 세트를 실행하십시오. BerryVM의 장기 운영 파이프라인에서는 점검 파일, 압축 파일 해시, 검수 로그를 동일한 배포 건의 기록으로 보관할 수 있지만, 로그에 자격 증명이나 마스킹하지 않은 환경 변수가 포함되어서는 안 됩니다.

최종 체크리스트는 간결하게 유지해야 합니다. 스테이징 디렉터리 동결, 심볼릭 링크 기록, 확장 속성 점검, 압축 파일 해시 생성, 별도 디렉터리 압축 해제, 파일 목록 일치, 실행 비트 확인, 해당되는 서명 검사 통과만 확인하면 됩니다. 이 과정을 갖추면 패키징은 빌드가 끝난 뒤 관성적으로 수행하는 단계가 아니라, 반복 실행할 수 있고 실패 원인을 정확히 드러내는 엔지니어링 게이트가 됩니다.

자주 묻는 질문

macOS 산출물에 zip -r만 사용하면 왜 위험한가요?

일반 ZIP 절차는 리소스 포크, 확장 속성, ACL 또는 심볼릭 링크 의미를 빠뜨릴 수 있습니다. 앱 번들이나 프레임워크는 ditto로 패키징하고 다시 검증하는 편이 안전합니다.

패키징 전에 xattr -cr을 실행해도 되나요?

전체 디렉터리에 일괄 적용하는 것은 권장하지 않습니다. 먼저 속성 목록과 용도를 확인하고, 불필요하다고 검증된 속성만 좁은 범위에서 처리해야 합니다.

압축 파일의 SHA-256만 같으면 검증이 끝난 건가요?

아닙니다. 해시는 압축 파일이 바뀌지 않았다는 사실만 보여 줍니다. 별도 위치에 풀어서 구조, 링크, 속성과 해당되는 경우 코드 서명까지 확인해야 합니다.

전용 물리 Mac

다음 개발 작업을 클라우드 Mac에서 시작하세요

Berry M4 또는 Berry M4 Pro를 선택하고 5개 노드, 대여 기간 및 추가 옵션을 확인한 뒤 주문을 제출하세요.

구성 선택 및 주문