diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d7aff9..2a76126 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,8 +170,106 @@ jobs: with: python-version: "3.12" - run: uv sync --no-install-project --group all + # * 실험 patch: macOS family lookup 에 시스템 폰트 사전 필터링 적용 — match_family_style 호출 전에 멤버십 체크해 missing family 를 CoreText 에 던지지 않음. external/rhwp 자체에는 commit 하지 않고 runtime 에만 apply. + - name: Apply experimental CoreText fix (macOS only) + if: runner.os == 'macOS' + run: | + cd external/rhwp + git apply ../../ci/patches/macos-coretext-fix.patch + echo "=== patch applied ===" + git diff --stat - run: uv run maturin develop --release - - run: uv run pytest tests/ -m "not slow" -v + # * macOS PNG 렌더 hang — 3차 진단으로 CoreText downloadable-font IPC hang 확인 (TDownloadableFontManager::Download → FontRegistryUI). chain 첫 폰트 부재 시 macOS 가 다운로드 시도 → daemon 응답 없음 → 무한 IPC wait. + # mitigation: 상류 fallback chain 의 모든 한글 폰트 사전 설치로 다운로드 lookup 자체를 막음. + - name: Install Korean font chain (macOS mitigation) + if: runner.os == 'macOS' + run: | + brew install --cask \ + font-noto-sans-kr \ + font-noto-serif-kr \ + font-noto-sans-cjk \ + font-nanum-gothic + echo "=== fc-list 한글 폰트 ===" + fc-list :lang=ko 2>&1 | head -20 || echo "(fc-list 미인식)" + - name: Run pytest (non-macOS) + if: runner.os != 'macOS' + run: uv run pytest tests/ -m "not slow" -v + - name: Run pytest (macOS) — sample native stack on hang + if: runner.os == 'macOS' + env: + RUST_BACKTRACE: full + run: | + set +e + mkdir -p /tmp/diag + uv run pytest tests/test_render_png.py::TestRenderPng::test_returns_png_magic -v -s & + PYTEST_PID=$! + # ^ 정상이면 분 단위 안에 끝남. 90초 wait 후 살아있으면 hang 으로 판정. + for i in $(seq 1 9); do + sleep 10 + if ! kill -0 "$PYTEST_PID" 2>/dev/null; then + wait "$PYTEST_PID"; EXIT=$? + echo "pytest 종료 in ~$((i*10))s exit=$EXIT" + if [ "$EXIT" = "0" ]; then + exec uv run pytest tests/ -m "not slow" -v + fi + exit $EXIT + fi + done + echo "=== HANG DETECTED — sampling native stacks ===" + # ^ 자식까지 포함 (uv → python pytest). sample 출력을 파일로 받아 artifact 업로드. + PIDS="$PYTEST_PID $(pgrep -P "$PYTEST_PID" || true)" + for PID in $PIDS; do + kill -0 "$PID" 2>/dev/null || continue + OUT="/tmp/diag/sample_${PID}.txt" + echo "----- sampling PID=$PID → $OUT -----" + sample "$PID" 15 -file "$OUT" 2>&1 | head -3 + echo ">>> Call graph head (PID=$PID) <<<" + awk '/^Call graph:/,/^Binary Images:/' "$OUT" 2>/dev/null | head -120 || echo "(파일 미생성)" + echo + done + ls -la /tmp/diag/ + kill -9 "$PYTEST_PID" 2>/dev/null + exit 1 + - name: Upload sample diagnostic artifacts + if: always() && runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: macos-png-hang-samples + path: /tmp/diag/ + if-no-files-found: warn + # * 상류 CLI 자체도 hang 하는지 확인 — rhwp export-svg 가 같은 FontMgr 진입점을 거침. external/rhwp 디렉토리에서 빌드하되 산출물은 workspace target 으로 떨어질 수 있어 두 path 다 시도. + - name: Upstream rhwp CLI 재현 (macOS, export-svg) + if: always() && runner.os == 'macOS' + working-directory: external/rhwp + run: | + set +e + cargo build --release --bin rhwp 2>&1 | tail -3 + BIN=../../target/release/rhwp + [ -x "$BIN" ] || BIN=./target/release/rhwp + [ -x "$BIN" ] || { echo "(빌드 산출물 없음)"; find ../.. -name "rhwp" -type f -maxdepth 5 2>/dev/null | head -5; exit 0; } + mkdir -p /tmp/svg_diag + echo "=== export-svg page 0, 90s soft timeout · using $BIN ===" + # macOS 는 GNU `timeout` 미포함 — background + sleep loop 로 구현 + "$BIN" export-svg samples/aift.hwp -o /tmp/svg_diag -p 0 & + CLI_PID=$! + START=$(date +%s) + while kill -0 "$CLI_PID" 2>/dev/null; do + sleep 5 + ELAPSED=$(( $(date +%s) - START )) + if [ "$ELAPSED" -ge 90 ]; then + echo "⚠️ TIMEOUT ${ELAPSED}s — 상류 CLI 도 동일하게 hang" + kill -9 "$CLI_PID" 2>/dev/null + wait "$CLI_PID" 2>/dev/null + exit 0 + fi + done + wait "$CLI_PID"; RC=$? + ELAPSED=$(( $(date +%s) - START )) + if [ "$RC" = "0" ]; then + echo "✓ 상류 CLI export-svg ${ELAPSED}s 에 정상 종료 — PNG path 특이성" + else + echo "exit=$RC after ${ELAPSED}s (timeout 도 정상도 아님)" + fi # * PDF 렌더링 — 느려서 별도 잡, Linux wheel 재사용 test-slow: diff --git a/ci/patches/macos-coretext-fix.patch b/ci/patches/macos-coretext-fix.patch new file mode 100644 index 0000000..0dc5674 --- /dev/null +++ b/ci/patches/macos-coretext-fix.patch @@ -0,0 +1,47 @@ +diff --git a/src/renderer/skia/renderer.rs b/src/renderer/skia/renderer.rs +index eea1f807..5193fedf 100644 +--- a/src/renderer/skia/renderer.rs ++++ b/src/renderer/skia/renderer.rs +@@ -2,7 +2,7 @@ use skia_safe::{ + font, paint, surfaces, Canvas, Color, EncodedImageFormat, Font, FontMgr, FontStyle, Paint, + PathBuilder, PathEffect, Rect, Typeface, + }; +-use std::collections::HashMap; ++use std::collections::{HashMap, HashSet}; + + use crate::error::HwpError; + use crate::model::image::ImageEffect; +@@ -22,13 +22,20 @@ pub struct SkiaLayerRenderer { + /// key = primary face name (Typeface::family_name), value = Typeface. + /// SVG 의 `--font-path` 와 같은 패턴으로 ttfs 디렉토리의 한컴 전용 폰트 (HY견명조 등) 도 사용 가능. + custom_typefaces: HashMap, ++ /// [diagnostic patch] 시스템 폰트 family list 사전 캐시 — macOS 의 ++ /// CoreText downloadable-font lookup IPC hang 회피용. match_family_style ++ /// 호출 전에 멤버십 체크해 missing family 를 CoreText 에 던지지 않음. ++ system_families: HashSet, + } + + impl SkiaLayerRenderer { + pub fn new() -> Self { ++ let font_mgr = FontMgr::default(); ++ let system_families: HashSet = font_mgr.family_names().collect(); + Self { +- font_mgr: FontMgr::default(), ++ font_mgr, + custom_typefaces: HashMap::new(), ++ system_families, + } + } + +@@ -326,6 +333,11 @@ impl SkiaLayerRenderer { + } + } + for family in &families { ++ // [diagnostic patch] macOS 의 CoreText downloadable lookup IPC ++ // hang 회피 — 시스템에 없는 family 는 match_family_style 에 던지지 않음. ++ if !self.system_families.contains(AsRef::::as_ref(family)) { ++ continue; ++ } + if let Some(tf) = self.font_mgr.match_family_style(family, font_style) { + push(&mut chain, &mut seen, tf); + }