본문 바로가기
C.W.K.
Lesson 02 of 04 · published

빌드, 설치, 닥터, 스모크

~15 min · 빌드 스크립트, 설치, 롤백, 스모크 테스트, 격리

Level 0새 탭
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

초록 빌드가 설치된 브라우저는 아니야

Voyager 집 간판엔 여러 군데서 되풀이되는 줄이 있어. 확인은 swift build가 아니라 /Applications/Voyager.app에서 해. 컴파일이 통과했다는 건 타입이 맞는다는 뜻일 뿐이야. 아빠가 쓰는 건 Applications에 있는 번들이야. 서명돼 있고, 아이콘이랑 버전이 붙어 있고, 아빠 세션이 들어 있는 바로 그 번들. 그래서 scripts/build-app.sh는 빌드, 번들, 서명, 확인, 설치를 트랜잭션 하나로 다뤄. 전부 되든지, 아니면 아빠의 설치본을 손대지 않은 채로 두든지.

스테이징, 서명, 확인

  1. 일찍 거절해. cwk-product.json의 버전이 제대로 읽혀야 하고(Voyager는 아직 1.0 전이야), 서명 아이덴티티가 설치돼 있어야 하고, 아이콘이 있어야 해. 전부 컴파일 시작 전에 봐.
  2. 빌드하고 번호 매겨. 릴리스로 빌드하고, 빌드 번호는 저장소 커밋 수로 매겨. 그러면 번들마다 역사의 어느 지점에서 빌드됐는지 알 수 있어.
  3. 스테이징. 번들은 .build 아래 임시 폴더에서 조립해. 바이너리, Info.plist, 아이콘, cwk-product.json 사본, 그리고 빌드할 때 키트에서 그려 오는 피파 마크. 저장소엔 마크 사본을 안 두거든.
  4. 서명하고 다시 읽어. 지난 레슨의 아이덴티티로 서명하고, 깊고 엄격하게 확인한 다음 서명 주체, 런타임, designated requirement를 봐. 통과한 번들만 build/로 복사돼.

인터럽트도 exit trap으로 돌려 놔서 Control-C로 멈춘 빌드도 스테이징 폴더를 치우고 가. 스크립트가 지우는 경로는 하나하나 자기 소유 목록이랑 대조해.

돌고 있는 브라우저 위에 설치하기

아빠 Voyager는 대개 켜져 있어. 설치는 확인된 번들을 /Applications에 숨긴 이름으로 복사해. 이름엔 스크립트 프로세스 id가 박혀 있고, 이 사본도 확인해. 그런 다음 돌고 있는 Voyager한테 SIGTERM으로 종료를 부탁해(첫 번째 블록). Voyager는 SIGTERM을 받으면 아빠가 메뉴에서 종료할 때랑 똑같이 세션을 저장해. 그래서 탭이 다시 돌아와. 스크립트는 최대 10초 기다려. 페이지가 대화 상자를 붙잡고 있다든가 해서 그래도 돌고 있으면, 스크립트는 멈추고 그렇다고 말해. 죽이는 건 없어.

Voyager가 꺼지면 예전 번들을 옆으로 치우고, 새 번들을 제자리에 넣고, 거기서 다시 확인해. 그 확인을 통과하기 전까진 exit trap이 예전 번들을 되돌릴 준비를 하고 있어. Launch Services한테 알리는 건 그다음이고, 되면 좋고 아니면 마는 단계야. 등록이 실패해도 무시하고 설치를 끝난 걸로 친 다음 예전 번들을 지워. trap도 못 돌 만큼 세게 죽은 설치는 pid 박힌 찌꺼기를 남기는데, 다음 실행이 프로세스가 죽은 것만 골라서 지워.

닥터는 읽기만 해

scripts/doctor.sh는 뭐가 빌드됐고, 뭐가 설치됐고, Voyager가 뭘 들고 있는지 답해. 그러면서 하나도 안 바꿔. 제품 버전이랑 번들 자체 번호는 따로 보고해. 서로 다른 질문의 답이니까. 서명 아이덴티티를 말하고, 시스템 WebKit 버전도 말해. 엔진은 시스템 거라서 Voyager는 보고만 하고 절대 고정하지 않아. 빌드본과 설치본마다 버전, 빌드, 서명 상태, 서명 주체, 런타임, requirement, 실행 파일 해시를 찍어. Voyager 폴더에 대해선 개수만 찍어. 저장된 세션의 탭, 아이덴티티, 기록된 이탈과 그중 이유 없는 것. 문제가 하나라도 있으면 개수를 달고 VOYAGER_DOCTOR_FAIL로 끝나.

아빠 상태에 손댈 수 없는 확인

진짜 앱을 돌리는 스모크 테스트는 진짜 앱 폴더, 키체인, 피파에 써. Voyager는 그 문을 아이덴티티로 닫아. 그게 키트의 격리 게이트야(두 번째 블록). build-app.sh --verification은 com.cwk.voyager.verify라는 별도 번들을 만들고, URL 타입도 문서 타입도 빼서 macOS가 절대 브라우저로 내놓지 않게 해. --isolated를 받아 주는 건 이 번들뿐이야. 출시용 앱한테 격리로 돌라고 하면 거절 문구를 찍고 종료해. 확인이 슬쩍 진짜 폴더로 넘어가는 게 바로 이 게이트가 막으려는 사고니까. 대역 피파 주소 같은 덮어쓰기 값은 격리 밖에선 비어 있는 걸로 읽히고, 격리 안에서도 피파 주소는 loopback이어야 해.

격리 실행은 버리는 컨테이너에서 살아. 가족 쪽엔 아무 데도 안 닿는 설정(카탈로그 없음, 빈 새 탭, 페이지 확대 1)으로 시작하고, 저장한 로그인은 키체인 대신 메모리 금고에 둬.

smoke.sh는 앱을 두 번 걸어

scripts/smoke.sh는 검증 앱을 빌드하고 loopback 포트에 대역 피파를 띄워. 앞에 무슨 페이지가 있는지 묻고 들은 걸 받아 적는 패널이 있는 작은 서버야. 비밀번호 칸이 있는 테스트 페이지랑 로그인 폼도 같이 내놔. 그리고 앱을 두 번 띄워(세 번째 블록). 한 번은 써 보려고, 한 번은 세션이 돌아오는지 보려고. 실행마다 60초 제한이야. check 줄 58개가 보는 건 이래. 창과 탭, 찾기, 확대, 북마크, 고정과 끌기, Incognito, 홈, 마우스 옆 버튼, 탭을 바꾼 뒤의 키 입력, 설정 창, 비밀번호 값은 빼고 페이지 얘기를 들은 패널, 저장했다가 다시 채운 로그인, 공유했다가 지운 동반 소스, 고정 탭이 된 피파 본인, 앞 탭만 깨어난 복원. 끝나면 컨테이너, 검증 빌드의 WebKit 데이터, 그 설정까지 지워.

원칙: 진짜 상태에 손댈 수 있는 확인은 언젠가 손대. 경계는 누가 기억해서 넘기는 플래그가 아니라 빌드의 성질로 만들어.

Code

cwkVoyager, scripts/build-app.sh: 돌고 있는 사본에 종료를 부탁하고, 절대 죽이지 않기 (원문 그대로, 221-234행)·shell
# Voyager saves its session when it is asked to quit, and on SIGTERM. It is
# asked first; nothing is killed.
running_pid() { /usr/bin/pgrep -f '^/Applications/Voyager\.app/Contents/MacOS/Voyager' 2>/dev/null | /usr/bin/head -n 1 }
if [[ -n "$(running_pid)" ]]; then
  /bin/kill -TERM "$(running_pid)" 2>/dev/null || true
  for _ in {1..40}; do
    [[ -z "$(running_pid)" ]] && break
    /bin/sleep 0.25
  done
fi
if [[ -n "$(running_pid)" ]]; then
  print -u2 "HARD_FAIL Voyager is still running (a page may be holding a dialog open); quit it, then run this again"
  exit 1
fi
cwkVoyager, Sources/VoyagerApp/VoyagerRuntime.swift: 격리는 검증 빌드에서만, 거기서 피파는 loopback에서만 (원문 그대로, 11-40행)·swift
    /// Honoured only in the verification build (the kit's rule): the shipping
    /// app cannot be pointed at a throwaway container.
    static let isolation: IsolationGate = {
        let gate = IsolationGate(requiredBundleIdentifier: VoyagerPaths.verificationBundleIdentifier)
        // Two launches of one verification run share a container, so the
        // second can restore what the first saved.
        if let run = gate.override("VOYAGER_ISOLATED_RUN"),
           !run.isEmpty, run.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "-" }) {
            return IsolationGate(requiredBundleIdentifier: VoyagerPaths.verificationBundleIdentifier, runIdentifier: run)
        }
        return gate
    }()
    static let paths = VoyagerPaths(isolation: isolation)

    /// The product version, from `cwk-product.json`. Nil when the manifest
    /// cannot be read; it is never guessed.
    static let productVersion: String? = {
        try? ProductManifest.load(using: BundledResourceLocator(), expecting: productID).version
    }()

    /// Where Pippa is. The installed app reaches her on this Mac. A
    /// verification run reaches only the stand-in its script names, never
    /// the real one: a check must not write into her store.
    static let pippaLocation: PippaLocation? = {
        guard isolation.isPermitted else { return .thisMac }
        guard let base = isolation.override("VOYAGER_PIPPA_BASE").flatMap(URL.init(string:)),
              ["127.0.0.1", "localhost"].contains(base.host() ?? "")
        else { return nil }
        return PippaLocation(base: base)
    }()
cwkVoyager, scripts/smoke.sh: 격리 실행 하나를 두 번 띄우고, 매번 마감 시간 두기 (원문 그대로, 96-111행)·shell
for phase in first restore; do
  VOYAGER_ISOLATED_RUN="$run" VOYAGER_SMOKE_PHASE="$phase" VOYAGER_SMOKE_REPORT="$work/$phase.json" \
    VOYAGER_PIPPA_BASE="$pippa_base" "$binary" --isolated &
  pid=$!
  # A run that hangs is a failure, not a wait.
  for _ in {1..240}; do
    /bin/kill -0 "$pid" 2>/dev/null || break
    /bin/sleep 0.25
  done
  if /bin/kill -0 "$pid" 2>/dev/null; then
    /bin/kill -TERM "$pid" 2>/dev/null || true
    print -u2 "VOYAGER_SMOKE_FAIL the $phase run did not finish within 60 s"
    exit 1
  fi
  [[ -f "$work/$phase.json" ]] || { print -u2 "VOYAGER_SMOKE_FAIL the $phase run wrote no report"; exit 1 }
done

External links

Exercise

내보내는 Mac 앱의 설치 스크립트를 써 봐. 새 번들을 설치본 옆에 스테이징하고 서명을 확인해. 돌고 있는 사본한테 SIGTERM으로 종료를 부탁하고 정해진 시간만 기다려. 예전 번들은 옆에 치워 두고 번들을 바꿔 끼우고, 뒤 단계가 하나라도 실패하면 exit trap에서 되돌려. 이어서 자기 번들 식별자를 가진 검증 빌드를 만들어. 임시 폴더를 상대로 돌 수 있는 건 그 빌드뿐이고, 출시용 빌드는 그렇게 돌라는 말을 들으면 종료하게 해.
Hint
롤백은 일부러 시험해. 바꿔 끼운 뒤 마지막 확인이 실패하게 만들고, 끝난 다음 /Applications에 예전 번들이 있는지 봐. 격리 스위치는 스크립트가 아니라 앱 안에서 번들 식별자를 확인해. 그래야 출시용 바이너리를 어떤 식으로 띄워도 그 스위치를 못 켜.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.