기술 운영 기록

클라우드 Mac CI에서 최종 Info.plist 감사하기

클라우드 Mac CI에서 최종 Info.plist 감사하기

같은 iOS 프로젝트라도 로컬에서는 문제없이 아카이브되지만, 클라우드 Mac의 Release 파이프라인에서는 잘못된 Bundle ID나 디버그 URL Scheme이 포함되거나 카메라 권한 설명이 누락될 수 있습니다. 원인은 대개 저장소에 있는 Info.plist 자체가 아니라, Xcode가 빌드 설정과 생성된 구성, target 속성을 병합해 App에 기록한 최종 결과에 있습니다. 신뢰할 수 있는 CI라면 소스 파일만 텍스트로 검사하지 말고 실제 빌드 산출물을 감사해야 합니다.

실제로 배포되는 구성부터 확인하기

최신 Xcode 프로젝트는 Info.plist를 자동으로 생성할 수도 있고, 서로 다른 target과 configuration, .xcconfig에서 값을 가져올 수도 있습니다. INFOPLIST_FILE이 가리키는 파일은 여러 입력 중 하나일 뿐이며, PRODUCT_BUNDLE_IDENTIFIER, MARKETING_VERSION, CURRENT_PROJECT_VERSION 같은 설정도 빌드 과정에서 함께 병합됩니다.

먼저 workspace, scheme, configuration, 출력 디렉터리를 명시해 빌드합니다.

set -euo pipefail

ROOT="$(pwd)"
DERIVED_DATA="$ROOT/.ci/DerivedData"

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath "$DERIVED_DATA" \
  CODE_SIGNING_ALLOWED=NO \
  build

여기서 서명을 비활성화하는 것은 빠른 구성 감사에만 적합합니다. 실제 배포용 아카이브에서도 서명을 꺼야 한다는 뜻은 아닙니다. 빌드가 끝난 뒤 App 경로를 추측하지 말고, 빌드 설정에서 제품 디렉터리와 이름을 읽어 옵니다.

SETTINGS="$(mktemp)"
xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath "$DERIVED_DATA" \
  -showBuildSettings > "$SETTINGS"

BUILD_DIR="$(awk -F ' = ' '/ TARGET_BUILD_DIR = /{print $2; exit}' "$SETTINGS")"
WRAPPER_NAME="$(awk -F ' = ' '/ WRAPPER_NAME = /{print $2; exit}' "$SETTINGS")"
APP_PATH="$BUILD_DIR/$WRAPPER_NAME"
PLIST_PATH="$APP_PATH/Info.plist"

test -d "$APP_PATH"
test -f "$PLIST_PATH"
plutil -lint "$PLIST_PATH"

감사 대상은 현재 작업에서 방금 생성한 App이어야 합니다. 이전 빌드가 남긴 고정 경로를 재사용하면 게이트가 오래된 산출물을 검사하고 잘못된 결론을 내릴 수 있습니다.

기대값을 버전 관리에 포함하기

많은 기대값을 CI 플랫폼 화면에 직접 하드코딩하지 마십시오. 각 배포 환경마다 작은 JSON 파일을 저장하는 편이 검토하기 쉽습니다. 예를 들어 ci/plist-release.json을 사용할 수 있습니다.

{
  "bundleIdentifier": "com.example.product",
  "urlSchemes": ["example"],
  "requiredUsageKeys": [
    "NSCameraUsageDescription",
    "NSPhotoLibraryUsageDescription"
  ],
  "allowedBackgroundModes": ["remote-notification"]
}

이 파일에는 저장소에 공개해도 되는 규칙만 기록하고, 토큰이나 개인 키, 서명 암호는 넣지 않습니다. 버전 번호와 빌드 번호는 보통 파이프라인 매개변수로 생성됩니다. 따라서 특정 숫자를 장기간 고정해 두기보다 스크립트에서 형식과 빌드 입력값의 일치 여부를 확인해야 합니다.

서로 다른 App target, 확장 프로그램, 테스트 호스트에는 각각 별도의 규칙이 필요합니다. 기본 App의 Bundle ID 규칙으로 Widget을 잘못 판정해서는 안 됩니다. 확장 프로그램에 카메라 권한이 필요하지 않는데도 카메라 설명이 포함된 동일한 체크리스트를 재사용해서도 안 됩니다.

실패할 수 있는 감사 스크립트 작성하기

plutil -extract는 딕셔너리와 배열을 읽는 데 적합하고, PlistBuddy는 단순한 스칼라 값을 읽을 때 유용합니다. 다음 스크립트는 최소한의 골격을 보여 줍니다.

#!/bin/bash
set -euo pipefail

PLIST_PATH="${1:?missing Info.plist path}"
EXPECTED_BUNDLE_ID="${EXPECTED_BUNDLE_ID:?missing bundle id}"
EXPECTED_VERSION="${EXPECTED_VERSION:?missing version}"
EXPECTED_BUILD="${EXPECTED_BUILD:?missing build number}"

read_key() {
  /usr/libexec/PlistBuddy -c "Print :$1" "$PLIST_PATH" 2>/dev/null
}

require_key() {
  local key="$1"
  local value
  value="$(read_key "$key" || true)"
  if [[ -z "$value" ]]; then
    printf 'Missing required key: %s
' "$key" >&2
    exit 1
  fi
}

[[ "$(read_key CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]]
[[ "$(read_key CFBundleShortVersionString)" == "$EXPECTED_VERSION" ]]
[[ "$(read_key CFBundleVersion)" == "$EXPECTED_BUILD" ]]

require_key NSCameraUsageDescription
require_key NSPhotoLibraryUsageDescription

SCHEMES_JSON="$(plutil -extract CFBundleURLTypes json -o - "$PLIST_PATH")"
printf '%s' "$SCHEMES_JSON" | grep -q '"example"'

MODES="$(plutil -extract UIBackgroundModes raw -o - "$PLIST_PATH" 2>/dev/null || true)"
if [[ "$MODES" == *"audio"* ]]; then
  printf 'Unexpected background mode: audio
' >&2
  exit 1
fi

운영 환경용 스크립트는 키 이름과 기대값, 실제값도 출력해야 하지만 전체 plist를 출력해서는 안 됩니다. 권한 문구, 조회 대상 Scheme, 서드파티 구성에는 내부 식별자가 포함될 수 있습니다. 원문 그대로 로그에 업로드하면 노출 범위가 커집니다.

허용 규칙과 금지 규칙을 함께 두기

“반드시 있어야 한다”는 규칙만으로는 누락된 설정은 찾을 수 있지만, 디버그 항목이 유출된 경우는 발견할 수 없습니다. Release 산출물에 테스트 서버 표식, 디버그 URL Scheme, 파일 공유 옵션, 승인되지 않은 백그라운드 모드가 포함되지 않도록 금지 목록도 함께 관리하는 것이 좋습니다.

배열 필드는 단순 문자열 포함 여부를 최종 검사 방식으로 사용하지 마십시오. plutil -extract ... json으로 JSON을 출력한 다음 Ruby, Python 또는 프로젝트에서 이미 사용하는 스크립트로 항목별 비교를 수행하는 편이 더 안정적입니다. 필드 누락, 잘못된 타입, 중복 값도 각각 명확하게 처리해야 합니다.

App, 확장 프로그램, 아카이브를 두 단계로 검사하기

하나의 아카이브에는 기본 App뿐 아니라 Widget, 알림 서비스 확장 프로그램, 기타 .appex가 함께 들어갈 수 있습니다. 기본 App만 검사하면 확장 프로그램의 식별자, 버전 번호, 권한 구성을 놓칠 수 있습니다. 아카이브가 완료된 뒤 모든 bundle을 순회할 수 있습니다.

find "$ARCHIVE_PATH/Products/Applications" \
  \( -name "*.app" -o -name "*.appex" \) -print0 |
while IFS= read -r -d '' bundle; do
  plist="$bundle/Info.plist"
  plutil -lint "$plist"
  printf 'Auditing %s
' "$bundle"
done

게이트는 두 단계로 두는 것이 좋습니다. 일반 Release build 후 빠른 검사를 수행해 조기에 피드백하고, archive가 끝난 뒤 아카이브 내부의 산출물을 다시 검사해 배포 여부를 결정합니다. 아카이브 작업에서 다른 configuration이나 내보내기 매개변수, 파이프라인 변수를 사용할 수 있으므로 두 번째 게이트는 생략할 수 없습니다.

검사 대상 주요 필드 실패 시 처리
기본 App 식별자, 버전, 권한 문구, URL Scheme 아카이브 차단
App Extension 식별자 접두사, 버전, 확장 유형 아카이브 차단
최종 Archive 모든 bundle과 운영 환경 금지 항목 배포 차단

흔한 오탐을 처리하고 증거 남기기

가장 흔한 오탐 원인은 빈 문자열, 불리언 값의 타입, 배열 순서입니다. 권한 설명은 키의 존재 여부만 확인해서는 안 되며, 앞뒤 공백을 제거한 뒤에도 값이 비어 있지 않은지 검사해야 합니다. 불리언 값은 plist 타입에 따라 읽어야 하며 문자열 "false"를 불리언 값으로 취급하면 안 됩니다. URL Scheme과 백그라운드 모드는 일반적으로 집합으로 비교하며 배열 순서에 의존하지 않아야 합니다.

스크립트가 실패하면 민감 정보를 제거한 감사 보고서를 남깁니다. 보고서에는 scheme, configuration, SDK, App 경로, 실패한 키 이름, 해시를 포함합니다. 전체 빌드 환경을 저장하거나 모든 환경 변수를 로그에 기록하지 마십시오. 같은 클라우드 Mac에서 여러 작업을 실행한다면 작업마다 독립된 DerivedData를 사용하고, 감사 전에 산출물의 수정 시간이 현재 작업에 해당하는지도 확인해야 합니다.

마지막으로 감사 스크립트도 코드처럼 관리해야 합니다. 규칙 변경은 병합 검토를 거치고, “키 누락, 잘못된 타입, 금지된 값, 확장 프로그램 누락”에 대해 각각 테스트 샘플을 준비합니다. 그러면 Info.plist 문제는 아카이브나 제출 단계에서 사람이 추측해야 하는 문제가 아니라, 파이프라인에서 명확하고 재현 가능한 실패로 처리됩니다.

자주 묻는 질문

소스 Info.plist만 검사하면 부족한 이유는 무엇인가요?

Xcode가 생성 값, 빌드 설정, 구성별 파일을 병합하기 때문입니다. 실제 배포되는 값은 빌드된 App 내부의 Info.plist에 있습니다.

CI에서 어떤 키부터 검사해야 하나요?

CFBundleIdentifier, 버전, 빌드 번호, 권한 설명, URL Scheme, UIBackgroundModes부터 검사하고 누락 값과 운영 금지 값은 실패 처리합니다.

감사는 archive 전과 후 중 언제 실행해야 하나요?

빠른 피드백을 위해 일반 빌드 후 실행하고, archive 완료 후 아카이브 내부 App을 다시 검사합니다. 배포 판정은 두 번째 결과를 기준으로 합니다.

MacVPSGo 클라우드 Mac

독립형 Apple Silicon 빌드 머신이 필요하신가요?

일·주·월·분기 단위로 독립형 물리 노드를 대여해 Xcode 빌드, iOS CI, 원격 개발 및 자동화 작업에 활용할 수 있습니다.

구성 선택 및 주문