HireVM 엔지니어링 기록

클라우드 Mac에서 SwiftPM 빌드 도구 플러그인 샌드박스 진단하기

클라우드 Mac에서 SwiftPM 빌드 도구 플러그인 샌드박스 진단하기

같은 Swift 패키지가 개발자 컴퓨터에서는 정상적으로 코드를 생성하지만, 클라우드 Mac의 깨끗한 작업 공간에서는 Operation not permitted 오류가 발생하거나 빌드에 성공해도 이전 파일이 컴파일되는 경우가 있습니다. 이는 대개 컴퓨터 성능 문제가 아니라 빌드 도구 플러그인이 선언되지 않은 파일에 의존하거나, 잘못된 디렉터리에 결과를 쓰거나, 로컬에 남아 있던 이전 산출물이 문제를 가리고 있기 때문입니다. 반복해서 캐시를 삭제하기보다 플러그인의 입력, 출력, 실행 경계를 하나씩 재구성해 확인하는 것이 핵심입니다.

먼저 어느 계층에서 실패하는지 확인하기

SwiftPM 빌드 도구 플러그인에는 세 계층이 관여합니다. SwiftPM은 명령 실행을 계획하고, 플러그인 실행 파일은 콘텐츠를 생성하며, Xcode는 생성된 파일을 사용합니다. 먼저 저장소 루트에서 전체 로그를 보존합니다.

set -o pipefail
rm -rf .ci
mkdir -p .ci
xcodebuild \
  -scheme App \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.ci/DerivedData" \
  -clonedSourcePackagesDirPath "$PWD/.ci/SourcePackages" \
  build 2>&1 | tee .ci/build.log

grep -E 'sandbox|deny|plugin|Operation not permitted|No such file' .ci/build.log

로그에 플러그인 도구 자체를 찾지 못했다고 표시되면 해당 도구가 패키지 타깃의 executable tool로 선언되어 있는지 확인합니다. 도구가 시작된 뒤 실패했다면 읽기 및 쓰기 경로를 계속 점검합니다. 빌드는 성공했지만 생성된 콘텐츠가 갱신되지 않았다면 입력과 출력 선언을 중점적으로 대조해야 합니다.

처음부터 샌드박스를 비활성화하는 방식으로 문제를 해결하려고 하지 마세요. 샌드박스 거부는 생성기가 빌드 그래프 밖의 콘텐츠를 읽거나 수정하고 있다는 사실을 정확히 알려주는 경우가 많습니다. 제한을 우회하면 다음 클린 빌드까지 문제를 미룰 뿐입니다.

입력과 출력을 빌드 그래프에 선언하기

안정적인 플러그인은 명시적으로 선언된 입력만 읽고 결과는 pluginWorkDirectory에 기록합니다. 다음 구조에서는 schema와 생성된 Swift 파일을 모두 SwiftPM이 추적합니다.

import PackagePlugin

@main
struct CodegenPlugin: BuildToolPlugin {
    func createBuildCommands(
        context: PluginContext,
        target: Target
    ) async throws -> [Command] {
        let tool = try context.tool(named: "schema-gen")
        let input = target.directory.appending("Schemas/api.json")
        let output = context.pluginWorkDirectory
            .appending("Generated/API.swift")

        return [
            .buildCommand(
                displayName: "Generate API.swift",
                executable: tool.path,
                arguments: [input.string, output.string],
                inputFiles: [input],
                outputFiles: [output]
            )
        ]
    }
}

생성기는 시작된 뒤 Generated 상위 디렉터리를 직접 만들고, 전달받은 출력 경로에만 파일을 써야 합니다. 저장소의 Sources, 데스크톱, 사용자 홈 디렉터리 또는 고정된 /Users/... 경로에 기본적으로 기록해서는 안 됩니다. 빌드 도구 플러그인은 현재 컴파일에 포함되는 파생 파일을 생성하는 용도입니다. 작업이 저장소 콘텐츠를 수정해야 한다면 빌드할 때마다 몰래 소스 코드를 변경하지 말고, 개발자가 명시적으로 실행하는 명령 플러그인으로 구현해야 합니다.

암시적 의존성 식별하기

흔한 암시적 입력으로는 현재 작업 디렉터리의 구성 파일, 환경 변수가 가리키는 템플릿, 홈 디렉터리의 캐시, 생성기가 자동으로 검색하는 전체 폴더 등이 있습니다. 다음 네 가지 질문을 항목별로 확인합니다.

점검 항목 올바른 방법 위험 신호
입력 파일 모두 inputFiles에 포함 실행 중 선언되지 않은 디렉터리를 재귀적으로 검색
출력 파일 모두 outputFiles에 포함 시간이나 컴퓨터에 따라 파일 이름이 달라짐
작업 경로 인수로 전달된 절대 경로 사용 pwd 또는 홈 디렉터리에 의존
생성 순서 정렬 후 일관되게 출력 파일 시스템 열거 순서에 의존

생성기 자체의 결정성 유지하기

권한이 올바르더라도 생성기 때문에 증분 빌드가 계속 무효화될 수 있습니다. 가장 흔한 원인은 파일 머리말에 현재 시간, 임시 디렉터리 또는 호스트 이름을 기록하는 것입니다. 같은 입력에서 매번 다른 바이트가 생성되면 SwiftPM은 실제 변경 여부를 판단할 수 없습니다.

클라우드 Mac에서 클린 생성을 두 번 연속 실행한 뒤 해시를 비교합니다.

rm -rf .ci/first .ci/second
mkdir -p .ci/first .ci/second

.build/debug/schema-gen \
  Schemas/api.json .ci/first/API.swift

.build/debug/schema-gen \
  Schemas/api.json .ci/second/API.swift

shasum -a 256 .ci/first/API.swift .ci/second/API.swift
cmp .ci/first/API.swift .ci/second/API.swift

두 해시는 반드시 같아야 합니다. 생성기는 필드, 파일, 선언도 정렬하고 줄바꿈 형식을 통일하며, 콘텐츠가 변경된 경우에만 대상 파일을 교체해야 합니다. 먼저 임시 파일에 쓴 다음 비교 후 원자적으로 이동하면 빌드가 중단될 때 불완전한 Swift 파일이 남는 것을 방지할 수 있습니다.

깨끗한 작업 공간에서 캐시 문제 재현하기

로컬에서 성공했다고 해서 선언이 완전한 것은 아닙니다. 로컬 DerivedData, 이전 플러그인 출력 또는 과거에 수동으로 생성한 소스 코드가 누락된 선언을 일시적으로 가릴 수 있습니다. 클라우드 빌드에서는 최소 한 번은 이전 상태가 전혀 없는 환경에서 검증한 다음, 두 번째 증분 빌드를 실행해야 합니다.

2회 검증 방법

첫 번째 실행에서는 전용 빌드 디렉터리를 삭제하고 빌드하여 모든 생성 파일이 플러그인에서 만들어지는지 확인합니다. 두 번째 실행에서는 입력을 수정하지 않은 채 다시 빌드하여 플러그인이 불필요하게 출력을 다시 쓰지 않는지 확인합니다. 그런 다음 schema 필드 하나만 변경하고 세 번째로 실행하여 해당 생성 파일이 실제로 갱신되는지 확인합니다.

다음 검사를 사용하면 생성 파일이 잘못된 위치에 기록되는 문제를 빠르게 찾을 수 있습니다.

find "$PWD" -type f -name 'API.swift' -print
git status --short

정상적인 경우 파생 파일은 빌드 디렉터리에 있고, git status에는 플러그인이 자동으로 수정한 버전 관리 대상 소스 코드가 나타나지 않아야 합니다. 두 번째 실행에서도 작업이 반복된다면 출력 파일이 실제로 존재하는지, 도구가 타임스탬프를 강제로 갱신하는지, 특정 입력 디렉터리가 지나치게 넓게 선언되었는지 확인합니다.

클라우드 Mac의 검증 경계 고정하기

HireVM의 원격 빌드 환경이나 그 밖의 깨끗한 macOS 노드에서는 수동 관찰에 의존하지 말고 다음 검사를 파이프라인에 포함하는 것이 좋습니다.

  1. 선택할 Xcode를 고정하고 로그 시작 부분에 xcodebuild -versionswift --version을 기록합니다.
  2. 작업 공간마다 별도의 DerivedData 및 패키지 체크아웃 디렉터리를 설정하여 병렬 작업이 서로의 파일을 덮어쓰지 않도록 합니다.
  3. 첫 빌드를 빈 디렉터리에서 시작하여 플러그인이 홈 디렉터리의 이전 파일에 의존하지 않는지 검증합니다.
  4. 플러그인 실패 전후의 상세 빌드 로그를 보존하되 토큰, 서명 자료 또는 전체 환경 변수를 로그에 기록하지 않습니다.
  5. 주요 생성 파일의 해시를 비교하여 같은 커밋에서 동일한 결과를 반복 생성할 수 있는지 확인합니다.
  6. 입력 하나를 수정한 뒤 다시 실행하여 증분 빌드가 예상한 출력만 갱신하는지 확인합니다.
  7. 콘솔에서 현재 선택 가능한 구성을 확인하고 동시 빌드 수에 맞춰 리소스를 선택합니다. 플러그인의 정확성 검사는 특정 컴퓨터의 우연한 상태에 의존해서는 안 됩니다.

최종 목표는 한 번의 빌드가 ‘우연히 성공’하게 만드는 것이 아니라 SwiftPM이 생성 단계를 완전히 이해하도록 하는 것입니다. 입력이 변경되면 정확히 다시 빌드하고, 입력이 같으면 불필요한 작업을 하지 않으며, 새로운 클라우드 Mac 작업 공간에서도 동일한 산출물을 생성해야 합니다. 이를 달성하면 샌드박스는 더 이상 장애물이 아니라 빌드 경계가 실제로 정확하고 완전한지 검증하는 자동 점검 장치가 됩니다.

자주 묻는 질문

SwiftPM 빌드 도구 플러그인이 소스 디렉터리를 직접 수정해도 되나요?

권장되지 않습니다. 생성 파일은 플러그인 작업 디렉터리에 기록하고 outputFiles로 선언해야 하며, 저장소를 수정하는 작업은 사용자가 명시적으로 실행하는 명령 플러그인으로 분리하는 편이 안전합니다.

로컬에서는 성공하지만 클라우드 Mac CI에서 샌드박스 오류가 나는 이유는 무엇인가요?

로컬 잔여 출력이 누락된 선언을 가리거나 생성기가 홈 디렉터리, 현재 디렉터리, 절대 경로에 의존하기 때문인 경우가 많습니다. 깨끗한 체크아웃과 고정된 빌드 경로로 다시 실행해야 합니다.

샌드박스를 끄면 문제를 해결할 수 있나요?

일시적으로 증상을 숨길 수 있지만 장기 해결책은 아닙니다. 모든 입력과 출력을 선언하고 허용된 디렉터리만 사용하도록 생성기를 수정해야 합니다.

전용 Apple Silicon 물리 노드

다음 원격 Mac 작업을 전용 노드에서 실행하세요

작업 규모에 맞춰 판매 중인 두 가지 구성, 다섯 개 노드와 일·주·월·분기 단위 중에서 선택하세요. 주문 전에 구성과 USD 금액을 모두 확인할 수 있습니다.

구성 선택 후 주문