{"id":8776,"date":"2025-08-28T17:20:37","date_gmt":"2025-08-28T08:20:37","guid":{"rendered":"https:\/\/hasu0707.duckdns.org\/blog\/?p=8776"},"modified":"2025-10-24T09:56:28","modified_gmt":"2025-10-24T00:56:28","slug":"fortify-unreal-engine-%eb%b6%84%ec%84%9d","status":"publish","type":"post","link":"https:\/\/hasu0707.duckdns.org\/blog\/?p=8776","title":{"rendered":"[Fortify] UnrealBuildTool(UnrealEngine) Wrapper (.uproject \ubd84\uc11d)"},"content":{"rendered":"\n<p>1_make_compile_commands_json.bat<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bat\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@SET UEBT_DIR=\"C:\\UnrealEngine-5.3.2-release\\Engine\\Binaries\\DotNET\\UnrealBuildTool\"\n@SET UE_PROJECT=\"C:\\UnrealEngine-5.3.2-release\\Samples\\Games\\Lyra\\Lyra.uproject\"\n@SET UE_TARGET=\"LyraGame Win64 Shipping\"\n\n@PUSHD %CD%\nCD \/D %UEBT_DIR%\nREM \ud544\uc694\ud55c \uacbd\uc6b0 generated \ud5e4\ub354 \uc0dd\uc131\nREM UnrealBuildTool.exe -Mode=Build -Project=%UE_PROJECT% -Target=%UE_TARGET%\nUnrealBuildTool.exe -Mode=GenerateClangDatabase -Project=%UE_PROJECT% -Target=%UE_TARGET%\n@POPD\n@PAUSE<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>2_ue5_fortify_translate.bat<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bat\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@CHCP 65001\n@CLS\nsourceanalyzer -b UE5_Test -clean\nDEL UE5_Test.fpr\nRMDIR \/S \/Q C:\\ue_fortify.tmp\npython ue5_fortify_translate.py ^\n--compile-db C:\\\\UnrealEngine-5.3.2-release\\\\compile_commands.json ^\n--staging C:\\\\ue_fortify.tmp\\\\staging ^\n--build-id UE5_Test ^\n--msvc \"C:\\\\Program Files\\\\Microsoft Visual Studio\\\\2022\\\\Community\\\\VC\\\\Tools\\\\MSVC\\\\14.36.32532\\\\bin\\\\Hostx64\\\\x64\\\\cl.exe\" ^\n--sourceanalyzer C:\\\\opentext\\\\sast-25.2.0\\\\bin\\\\sourceanalyzer.exe ^\n--max-workers 4\nsourceanalyzer -b UE5_Test -Xms2g -Xmx8g -Xss1m -scan -logfile scan.log -f UE5_Test.fpr\n@PAUSE<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>ue5_fortify_translate.py<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># UE5 \u2192 Fortify SCA Translation Runner (Windows \/ Python 3)\n# compile_commands.json \u2192 RSP\/GCD \ud45c\uc900\ud654(normalize) \u2192 Fortify SCA(sourceanalyzer) Translation\n# - \uc7ac\uadc0\uc801 @include \ud480\uae30, \uc808\ub300 \uacbd\ub85c\ud654, \uc635\uc158(\ubd84\ub9ac\/\uacb0\ud569\ud615) \uc815\uaddc\ud654\n# - UE \ubaa8\ub4c8 Public\/Private\/Classes + UHT Inc \uacbd\ub85c \uc790\ub3d9 \uc8fc\uc785(Engine &amp; Plugins, x64 \uc720\ubb34 \ubaa8\ub450 \ucc98\ub9ac)\n# - \ucf58\uc194\uc5d0\ub294 \uc548\uc804\ud55c \uc694\uc57d \ub85c\uadf8\ub9cc \ucd9c\ub825, \uac01 \uc720\ub2db\ubcc4 \uc804\uccb4 \ub85c\uadf8\ub294 \ud30c\uc77c\ub85c \uc800\uc7a5\n# - \uc0ac\uc804 \uc810\uac80(Preflight): UHT\uac00 \uc0dd\uc131\ud558\ub294 *.generated.h \uc5c6\uc73c\uba74 \uc548\ub0b4 \ud6c4 \uc885\ub8cc \ucf54\ub4dc 3 \ubc18\ud658\n\nfrom __future__ import annotations\nimport argparse\nimport hashlib\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n# ===== \uae30\ubcf8\uac12(\ub9ac\ud130\ub7f4 \ub0b4 \uacbd\ub85c\ub294 \uc815\ubc29\ud5a5 \uc2ac\ub798\uc2dc \uc720\uc9c0: \uce94\ubc84\uc2a4 \uc548\uc804\uc131) =====\nDEFAULT_MSVC = \"C:\/Program Files\/Microsoft Visual Studio\/2022\/Community\/VC\/Tools\/MSVC\/14.36.32532\/bin\/Hostx64\/x64\/cl.exe\"\nDEFAULT_SOURCEANALYZER = \"C:\/opentext\/sast-25.2.0\/bin\/sourceanalyzer.exe\"\nDEFAULT_BUILD_ID = \"UE5_Test\"\n\n# ===== \uc815\uaddc\uc2dd \/ \uc0c1\uc218 =====\nWIN_DRIVE_RE = re.compile(r\"^[A-Za-z]:\/\")\nQUOTE_RE = re.compile(r'^[\\\"\\'](.*)[\\\"\\']$')\nRSP_INCLUDE_LINE = re.compile(r'^@(?:\"(.*?)\"|\\'(.*?)\\'|(\\S+))\\s*$')  # @\"\u2026\" | @'\u2026' | @path\nANY_QUOTED = re.compile(r'\"(.*?)\"|\\'(.*?)\\'')\nFIRST_TOKEN_IS_PATH_RE = re.compile(r'^[\\\"\\'].*[\\\"\\']\\s*$')\nCOMMAND_EXTRACT_RSP = re.compile(r'@(?:\"(.*?\\.(?:rsp|gcd))\"|\\'(.*?\\.(?:rsp|gcd))\\'|(\\S+\\.(?:rsp|gcd)))')\n\n# \ubd84\ub9ac\ud615 \uc635\uc158(\/I &lt;dir> \ub4f1)\uacfc \uacb0\ud569\ud615 \uc635\uc158(\/IC:\\\\dir \ub4f1) \ubaa8\ub450 \uc9c0\uc6d0\nOPTION_PREFIXES_SPLIT = {'\/I','-I','\/FI','\/Fo','\/Fp','\/Fe','\/Fd','\/Fa','\/FU','\/external:I'}\nOPTION_PREFIXES_COMBINED = ['\/I','-I','\/FI','\/Fo','\/Fp','\/Fe','\/Fd','\/Fa','\/FU','\/external:I']\n\n# \uc774\ubbf8 \uc2a4\ud14c\uc774\uc9d5\ud55c RSP\/GCD\ub294 \uc7ac\ud65c\uc6a9(\uc7ac\uadc0 \ucc98\ub9ac \uce90\uc2dc)\nSTAGED_CACHE: Dict[Path, Path] = {}\n\n# ===== \uc720\ud2f8\ub9ac\ud2f0 =====\n\ndef dequote(s: str) -> str:\n    \"\"\"\uc591\ub05d \ub530\uc634\ud45c \uc81c\uac70.\"\"\"\n    m = QUOTE_RE.match(s)\n    return m.group(1) if m else s\n\n\ndef as_abs(path: str, base: Path) -> str:\n    \"\"\"\uacbd\ub85c\ub97c \uc808\ub300\uacbd\ub85c\ub85c \uc815\uaddc\ud654(\ud658\uacbd\ubcc0\uc218\/\ub530\uc634\ud45c \ucc98\ub9ac, \uc5ed\uc2ac\ub798\uc2dc\ub294 \uc815\ubc29\ud5a5\uc73c\ub85c \ud1b5\uc77c).\"\"\"\n    p = path.replace('\\\\', '\/')\n    p = os.path.expandvars(p)\n    p = dequote(p)\n    if not WIN_DRIVE_RE.match(p):\n        p = str((base \/ p).resolve())\n    return str(Path(p).resolve())\n\n\ndef split_preserving_quotes(s: str) -> List[str]:\n    \"\"\"\uacf5\ubc31 \ubd84\ud560 \uc2dc \ub530\uc634\ud45c \ub0b4\ubd80\ub294 \ubcf4\uc874\ud558\ub294 \uac04\ub2e8\ud55c \ud1a0\ud06c\ub098\uc774\uc800.\"\"\"\n    out: List[str] = []\n    cur: List[str] = []\n    quote: str | None = None\n    for ch in s:\n        if quote:\n            if ch == quote:\n                cur.append(ch)\n                out.append(''.join(cur).strip())\n                cur = []\n                quote = None\n            else:\n                cur.append(ch)\n        else:\n            if ch in ('\"', \"'\"):\n                if cur and not cur[-1].isspace():\n                    out.append(''.join(cur).strip()); cur = []\n                cur.append(ch); quote = ch\n            elif ch.isspace():\n                if cur:\n                    out.append(''.join(cur).strip()); cur = []\n            else:\n                cur.append(ch)\n    if cur:\n        out.append(''.join(cur).strip())\n    return [t for t in out if t]\n\n\ndef normalize_token_path(token: str, base: Path) -> str:\n    \"\"\"\ud1a0\ud070 \uc548\uc758 \uacbd\ub85c \uc694\uc18c\ub97c \uc808\ub300\uacbd\ub85c\/\ub530\uc634\ud45c \ud3ec\ud568 \ud615\ud0dc\ub85c \uc815\uaddc\ud654.\"\"\"\n    # @\"file.rsp\" \uac19\uc740 include \ub77c\uc778\n    m = RSP_INCLUDE_LINE.match(token)\n    if m:\n        inner = next((g for g in m.groups() if g), None)\n        if inner:\n            abs_inner = as_abs(inner, base)\n            return '@' + abs_inner\n\n    # \uacb0\ud569\ud615 \uc635\uc158(\/IC:\\\\inc \ub4f1) \ub0b4\ubd80 \uacbd\ub85c \uc815\uaddc\ud654\n    for pre in OPTION_PREFIXES_COMBINED:\n        if token.startswith(pre) and len(token) > len(pre) and '\"' not in token and \"'\" not in token:\n            path_part = token[len(pre):]\n            abs_p = as_abs(path_part, base)\n            return f'{pre}\"{abs_p}\"'\n\n    # \ub530\uc634\ud45c\ub85c \ub458\ub7ec\uc2f8\uc778 \ub0b4\ubd80 \uacbd\ub85c \uc815\uaddc\ud654\n    if '\"' in token or \"'\" in token:\n        def repl(mo):\n            inner = mo.group(1) or mo.group(2) or ''\n            return '\"' + as_abs(inner, base) + '\"'\n        return ANY_QUOTED.sub(repl, token)\n\n    # \uc18c\uc2a4\/\ud5e4\ub354\ub958 \ud30c\uc77c\uc774\uba74 \uc808\ub300\uacbd\ub85c+\ub530\uc634\ud45c\ub85c \uac10\uc2f8\uae30\n    low = token.lower()\n    if low.endswith('.c') or low.endswith('.cpp') or low.endswith('.cxx') or low.endswith('.cc') or low.endswith('.c++') or low.endswith('.rc') or low.endswith('.hpp') or low.endswith('.h') or low.endswith('.inl'):\n        return '\"' + as_abs(token, base) + '\"'\n    return token\n\n\ndef normalize_tokens_with_context(tokens: List[str], base: Path) -> List[str]:\n    \"\"\"\ubd84\ub9ac\ud615 \uc635\uc158\uc744 \uacb0\ud569\ud615\uc73c\ub85c \ubc14\uafb8\uace0, \uacbd\ub85c\ub97c \uc808\ub300\uacbd\ub85c\ub85c \ub9cc\ub4e0 \ud1a0\ud070 \ub9ac\uc2a4\ud2b8 \ubc18\ud658.\"\"\"\n    out: List[str] = []\n    i = 0\n    while i &lt; len(tokens):\n        t = tokens[i]\n        if t in OPTION_PREFIXES_SPLIT and i + 1 &lt; len(tokens):\n            nxt = tokens[i + 1]\n            abs_p = as_abs(dequote(nxt), base)\n            out.append(f'{t}\"{abs_p}\"')\n            i += 2\n            continue\n        out.append(normalize_token_path(t, base))\n        i += 1\n    return out\n\n# ===== UE \ub8e8\ud2b8\/\ubaa8\ub4c8 \ud0d0\uc9c0 \ubc0f UHT \uc874\uc7ac \uc5ec\ubd80 \uccb4\ud06c =====\n\ndef find_ue_root_from(path_hint: Path) -> Path | None:\n    \"\"\"\uc5b4\ub5a4 \uacbd\ub85c\uc5d0\uc11c\ub4e0 Engine \uc0c1\uc704(UE \ub8e8\ud2b8)\ub97c \ucd94\uc815.\"\"\"\n    p = path_hint.resolve()\n    for anc in [p] + list(p.parents):\n        if anc.name.lower() == 'engine' and anc.parent.exists():\n            return anc.parent\n    return None\n\n\ndef uht_inc_roots(ue_root: Path) -> List[Path]:\n    \"\"\"UHT\uac00 \uc0dd\uc131\ud558\ub294 Inc\/&lt;Module> \ub8e8\ud2b8 \ud6c4\ubcf4\ub4e4 \ubc18\ud658(\uc544\ud0a4\ud14d\ucc98 \ucf00\uc774\uc2a4 \ud3ec\ud568).\"\"\"\n    return [\n        ue_root \/ 'Engine\/Intermediate\/Build\/Win64\/UnrealEditor\/Inc',\n        ue_root \/ 'Engine\/Intermediate\/Build\/Win64\/x64\/UnrealEditor\/Inc',\n        ue_root \/ 'Engine\/Intermediate\/Build\/UnrealEditor\/Inc',\n    ]\n\n\ndef derive_module_roots_from_source(src_file: Path) -> List[Path]:\n    \"\"\"\uc18c\uc2a4 \ud30c\uc77c \uacbd\ub85c\uc5d0\uc11c \ubaa8\ub4c8 \uae30\uc900\uacbd\ub85c(&lt;Module>\/Source)\ub97c \ucc3e\uc544 \ubaa8\ub4c8 \ub8e8\ud2b8 \ud6c4\ubcf4 \ubc18\ud658.\"\"\"\n    parts = list(src_file.resolve().parts)\n    if 'Source' in parts:\n        idx = parts.index('Source')\n        if idx + 1 &lt; len(parts):\n            return [Path(*parts[:idx+2])]\n    return []\n\n\ndef append_module_and_uht_includes(lines_out: List[str], ue_root: Path, src_file: Path) -> None:\n    \"\"\"\ubaa8\ub4c8 Public\/Private\/Classes\uc640 Engine\/Plugin\uc758 UHT Inc \uacbd\ub85c\ub97c \/I \uc635\uc158\uc73c\ub85c \uc8fc\uc785.\"\"\"\n    added: set[str] = set()\n    def add_inc(p: Path):\n        p = p.resolve()\n        if p.is_dir():\n            sp = str(p)\n            if sp not in added:\n                lines_out.append(f'\/I\"{sp}\"')\n                added.add(sp)\n\n    # \uc18c\uc2a4 \ud30c\uc77c\uc758 \ub514\ub809\ud130\ub9ac\n    add_inc(src_file.parent)\n\n    # \ubaa8\ub4c8 \ub514\ub809\ud130\ub9ac\ub4e4\n    for mod_root in derive_module_roots_from_source(src_file):\n        for sub in ('Public','Private','Classes'):\n            add_inc(mod_root \/ sub)\n\n    # Engine \ucabd UHT Inc\/&lt;Module>\n    for root in uht_inc_roots(ue_root):\n        root = root.resolve()\n        if root.is_dir():\n            for child in root.iterdir():\n                if child.is_dir():\n                    add_inc(child)\n\n    # Plugins \ucabd UHT Inc\/&lt;Module>\n    plugins_root = (ue_root \/ 'Engine\/Plugins').resolve()\n    if plugins_root.is_dir():\n        for cat in plugins_root.iterdir():\n            if not cat.is_dir():\n                continue\n            for plug in cat.iterdir():\n                if not plug.is_dir():\n                    continue\n                for arch in ('Win64\/UnrealEditor\/Inc', 'Win64\/x64\/UnrealEditor\/Inc'):\n                    inc_root = (plug \/ f'Intermediate\/Build\/{arch}').resolve()\n                    if inc_root.is_dir():\n                        for child in inc_root.iterdir():\n                            if child.is_dir():\n                                add_inc(child)\n\n# ===== RSP\/GCD \uc815\uaddc\ud654 =====\n\ndef _staged_name_for(src: Path, staging_dir: Path) -> Path:\n    \"\"\"\uc6d0\ubcf8 \uacbd\ub85c \ud574\uc2dc\ub85c \uc2a4\ud14c\uc774\uc9d5 \ud30c\uc77c\uba85 \uc0dd\uc131(\ucda9\ub3cc \ubc29\uc9c0).\"\"\"\n    h = hashlib.md5(str(src.resolve()).encode('utf-8')).hexdigest()[:8]\n    return staging_dir \/ f\"{src.stem}.{h}{src.suffix}\"\n\n# ----- \uc804\ucc98\ub9ac \uc0b0\ucd9c\ubb3c \uc720\ubc1c \uc635\uc158 \uc81c\uac70(\/P, \/E, \/EP, \/Fi*) -----\n\ndef _drop_preprocess_token(tok: str, next_tok: str | None = None) -> int:\n    \"\"\"\n    \ubc18\ud658\uac12: \uac74\ub108\ub6f8 \ud1a0\ud070 \uac1c\uc218 (0=\uc720\uc9c0, 1=\ud604\uc7ac \ud1a0\ud070\ub9cc \uc81c\uac70, 2=\/Fi \ubd84\ub9ac\ud615\ucc98\ub7fc \uac12 \ub3d9\ubc18 \uc81c\uac70)\n    - \/P, \/E, \/EP: MSVC \uc804\ucc98\ub9ac \uc0b0\ucd9c\ubb3c\/\ud45c\uc900\ucd9c\ub825 \uc804\ucc98\ub9ac \u2192 *.i \uc4f0\ub808\uae30 \ud30c\uc77c \uc0dd\uc131 \uc720\ubc1c\n    - \/Fi*, '\/Fi' &lt;file>: \uc804\ucc98\ub9ac \uc0b0\ucd9c\ubb3c \ud30c\uc77c\uba85 \uc9c0\uc815(\ud63c\uc120 \uc720\ubc1c)\n    - (-E, -P): Clang \uacc4\uc5f4 \ubc29\uc5b4\n    \"\"\"\n    u = tok.upper()\n    if u in ('\/P', '\/E', '\/EP'):\n        return 1\n    if tok.startswith('\/Fi'):\n        return 1\n    if tok == '\/Fi' and next_tok is not None:\n        return 2\n    if tok in ('-E','-P'):\n        return 1\n    return 0\n\n\ndef process_rsp_like_file(src: Path, staging_dir: Path, base_cwd: Path, src_file_for_extra_inc: Path | None) -> Path:\n    \"\"\"RSP\/GCD\ub97c \uc77d\uc5b4 \uc7ac\uadc0\uc801\uc73c\ub85c \ud45c\uc900\ud654\ud558\uc5ec \uc2a4\ud14c\uc774\uc9d5 \ud30c\uc77c\ub85c \ub5a8\uc5b4\ub728\ub9bc.\"\"\"\n    src = src.resolve()\n    if src in STAGED_CACHE:\n        return STAGED_CACHE[src]\n\n    staging_dir.mkdir(parents=True, exist_ok=True)\n\n    try:\n        text = src.read_text(encoding='utf-8', errors='ignore')\n    except FileNotFoundError:\n        # \uc6d0\ubcf8\uc774 \uc5c6\uc73c\uba74 \uadf8\ub300\ub85c \ucc38\uc870\ub97c \ub0a8\uae34 stub \uc791\uc131\n        dst = _staged_name_for(src, staging_dir)\n        with dst.open('w', encoding='utf-8', newline='') as f:\n            f.write('@' + str(src) + '\\n')\n        STAGED_CACHE[src] = dst\n        return dst\n\n    lines_out: List[str] = []\n    for line in text.splitlines():\n        raw = line.strip()\n        if not raw:\n            lines_out.append(line)\n            continue\n\n        # @include \uc7ac\uadc0 \ucc98\ub9ac\n        m = RSP_INCLUDE_LINE.match(raw)\n        if m:\n            inc = next((g for g in m.groups() if g), None)\n            if inc:\n                abs_inc = Path(as_abs(inc, base_cwd))\n                staged_inc = process_rsp_like_file(abs_inc, staging_dir, base_cwd, src_file_for_extra_inc)\n                lines_out.append('@' + str(staged_inc))\n                continue\n\n        # \uccab \ud1a0\ud070\uc774 \uc778\uc6a9\ubd80 \uacbd\ub85c \ud55c \uac1c\ub9cc \uc788\ub294 \uacbd\uc6b0(\"C:\/...\/foo.cpp\") \uc808\ub300\uacbd\ub85c\ud654\n        if FIRST_TOKEN_IS_PATH_RE.match(raw) and (raw.startswith('\"') or raw.startswith(\"'\")):\n            abs_p = as_abs(raw, base_cwd)\n            lines_out.append('\"' + abs_p + '\"')\n            continue\n\n        # \uc77c\ubc18 \ud589: \ud1a0\ud070\ud654 \u2192 \uacbd\ub85c \uc808\ub300\ud654\/\uc815\uaddc\ud654 \u2192 \uc804\ucc98\ub9ac \uad00\ub828 \uc635\uc158 \uc81c\uac70\n        tokens = split_preserving_quotes(line)\n        norm_tokens = normalize_tokens_with_context(tokens, base_cwd)\n\n        cleaned: List[str] = []\n        i = 0\n        while i &lt; len(norm_tokens):\n            t = norm_tokens[i]\n            # \uc911\ubcf5\ub41c \/Fo \uc815\ub9ac\n            if t == '\/Fo' and i + 1 &lt; len(norm_tokens) and norm_tokens[i+1].startswith('\/Fo'):\n                i += 1; continue\n            # \uc804\ucc98\ub9ac \uc0b0\ucd9c\ubb3c \uad00\ub828 \ud50c\ub798\uadf8 \ub4dc\ub86d\n            skip = _drop_preprocess_token(t, norm_tokens[i+1] if i+1 &lt; len(norm_tokens) else None)\n            if skip:\n                i += skip\n                continue\n            cleaned.append(t); i += 1\n        lines_out.append(' '.join(cleaned))\n\n    # \uc18c\uc2a4 \ud30c\uc77c \ubb38\ub9e5 \uae30\ubc18 \ucd94\uac00 \/I \uacbd\ub85c \uc8fc\uc785\n    ue_root = find_ue_root_from(base_cwd) or find_ue_root_from(src) or base_cwd\n    if src_file_for_extra_inc is not None:\n        append_module_and_uht_includes(lines_out, ue_root, src_file_for_extra_inc)\n\n    # \uc2a4\ud14c\uc774\uc9d5 \ud30c\uc77c \uc800\uc7a5\n    dst = _staged_name_for(src, staging_dir)\n    with dst.open('w', encoding='utf-8', newline='') as f:\n        f.write('\\n'.join(lines_out)); f.write('\\n')\n\n    STAGED_CACHE[src] = dst\n    return dst\n\n# ===== Fortify \ud638\ucd9c =====\n\ndef build_sa_command(sourceanalyzer: Path, build_id: str, compiler_exe: Path, rsp_like: Path) -> List[str]:\n    \"\"\"sourceanalyzer \uc2e4\ud589 \ucee4\ub9e8\ub4dc \uad6c\uc131.\"\"\"\n    return [str(sourceanalyzer), '-b', build_id, '-debug', '-verbose', str(compiler_exe), '@' + str(rsp_like)]\n\n\ndef parse_compile_command(cmd: str) -> Tuple[Path | None, Path | None]:\n    \"\"\"compile_commands.json\uc758 command\uc5d0\uc11c \ucef4\ud30c\uc77c\ub7ec \uacbd\ub85c\uc640 RSP\/GCD \ucc38\uc870 \ucd94\ucd9c.\"\"\"\n    tokens = split_preserving_quotes(cmd)\n    compiler: Path | None = None\n    if tokens:\n        first = dequote(tokens[0])\n        if first.lower().endswith('.exe'):\n            compiler = Path(first)\n    m = COMMAND_EXTRACT_RSP.search(cmd)\n    rsp_like: Path | None = None\n    if m:\n        cand = next((g for g in m.groups() if g), None)\n        if cand:\n            rsp_like = Path(dequote(cand))\n    return compiler, rsp_like\n\n\ndef save_full_log(staging: Path, unit_name: str, content: str) -> Path:\n    \"\"\"\uc804\uccb4 \ub85c\uadf8\ub97c \uc2a4\ud14c\uc774\uc9d5 \ub514\ub809\ud130\ub9ac\uc5d0 \uc800\uc7a5\ud558\uace0 \uacbd\ub85c \ubc18\ud658.\"\"\"\n    safe = re.sub(r'[^A-Za-z0-9_.-]+', '_', unit_name)[:120]\n    p = staging \/ f'{safe}.fortify.log'\n    p.write_text(content, encoding='utf-8', errors='ignore')\n    return p\n\n\ndef summarize_output(out: str, max_len: int = 1000) -> str:\n    \"\"\"\ub85c\uadf8\uc758 \uc55e\/\ub4a4 \uc77c\ubd80\ub9cc \ud569\uccd0 \ucf58\uc194 \uc694\uc57d \uc0dd\uc131.\"\"\"\n    lines = [ln for ln in out.splitlines() if ln.strip()]\n    head = '\\n'.join(lines[:5])\n    tail = '\\n'.join(lines[-40:])\n    msg = (head + ('\\n...\\n' if len(lines) > 45 else '\\n') + tail)\n    if len(msg) > max_len:\n        msg = msg[-max_len:]\n    return msg\n\n\ndef run_translation_for_entry(entry: dict, args) -> Tuple[str, int, str]:\n    \"\"\"compile_commands.json\uc758 \ud55c \uc5d4\ud2b8\ub9ac\ub97c \ubc88\uc5ed(Translation) \uc2e4\ud589.\"\"\"\n    file_path = Path(entry.get('file', ''))\n    cmd_str = entry.get('command', '')\n    base_dir = Path(entry.get('directory', '.'))\n\n    compiler, rsp_like = parse_compile_command(cmd_str)\n    if compiler is None:\n        compiler = Path(args.msvc)\n\n    # RSP\/GCD\uac00 \uc788\ub294 \uacbd\uc6b0: \uc7ac\uadc0 \uc815\uaddc\ud654 \ud6c4 \uc2a4\ud14c\uc774\uc9d5 \ud30c\uc77c \uc0ac\uc6a9\n    if rsp_like is not None:\n        abs_rsp = (base_dir \/ rsp_like).resolve()\n        staged_rsp = process_rsp_like_file(abs_rsp, args.staging, base_dir, file_path)\n    else:\n        # RSP\uac00 \uc5c6\uc73c\uba74 command \ud1a0\ud070\uc744 \uc9c1\uc811 \uc815\uaddc\ud654\ud558\uc5ec \uc784\uc2dc RSP \uc0dd\uc131\n        temp_name = (file_path.name or 'unit').replace('.', '_') + '.autogen.rsp'\n        temp_path = args.staging \/ temp_name\n        tokens = split_preserving_quotes(cmd_str)\n        if tokens and tokens[0].lower().endswith('.exe'):\n            tokens = tokens[1:]\n        norm_tokens = normalize_tokens_with_context(tokens, base_dir)\n        lines_out = [' '.join(norm_tokens)]\n        ue_root = find_ue_root_from(base_dir) or base_dir\n        append_module_and_uht_includes(lines_out, ue_root, file_path)\n        with temp_path.open('w', encoding='utf-8', newline='') as f:\n            f.write('\\n'.join(lines_out)); f.write('\\n')\n        staged_rsp = temp_path\n\n    sa_cmd = build_sa_command(args.sourceanalyzer, args.build_id, compiler, staged_rsp)\n\n    # \uc2e4\ud589 \ucee4\ub9e8\ub4dc \ubc0f RSP \ud5e4\ub354 \uc77c\ubd80 \ucd9c\ub825(\ub514\ubc84\uadf8 \uac00\ub3c5\uc131)\n    print('[SA-CMD] ' + ' '.join(sa_cmd))\n    try:\n        head = Path(staged_rsp).read_text(encoding='utf-8', errors='ignore').splitlines()[:20]\n        print('[RSP-HEAD]')\n        sys.stdout.write('\\n'.join(head) + '\\n')\n    except Exception:\n        pass\n\n    # Fortify \uc2e4\ud589\n    try:\n        proc = subprocess.run(\n            sa_cmd,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            text=True,\n            encoding='mbcs',\n            errors='replace'\n        )\n        rc = proc.returncode\n        out = proc.stdout\n    except Exception as e:\n        rc = -1\n        out = f\"EXEC ERROR: {e}\"\n\n    unit_desc = str(file_path) if file_path else str(staged_rsp)\n\n    # \ub85c\uadf8 \uc800\uc7a5 + \uc694\uc57d \ubc18\ud658\n    log_path = save_full_log(args.staging, Path(unit_desc).name, out)\n    short_log = summarize_output(out)\n    short_log += f\"\\n[full log] {log_path}\"\n\n    return unit_desc, rc, short_log\n\n# ===== \uba54\uc778 \/ \uc0ac\uc804 \uc810\uac80 =====\n\ndef main(argv: List[str] | None = None) -> int:\n    p = argparse.ArgumentParser(description='UE5 \u2192 Fortify SCA Translation Runner (recursive @include + module incs)')\n    p.add_argument('--compile-db', required=True, type=Path, help='compile_commands.json \uacbd\ub85c')\n    p.add_argument('--staging', required=True, type=Path, help='\uc815\uaddc\ud654\ub41c rsp\/gcd\ub97c \uc800\uc7a5\ud560 \ub514\ub809\ud130\ub9ac')\n    p.add_argument('--build-id', default=DEFAULT_BUILD_ID)\n    p.add_argument('--msvc', default=DEFAULT_MSVC, type=Path, help='\ucef4\ud30c\uc77c\ub7ec \uacbd\ub85c(\uae30\ubcf8\uac12)')\n    p.add_argument('--sourceanalyzer', default=DEFAULT_SOURCEANALYZER, type=Path)\n    p.add_argument('--max-workers', type=int, default=os.cpu_count() or 4)\n    p.add_argument('--filter-ext', nargs='*', default=['.c', '.cpp', '.cxx', '.cc', '.c++', '.cs'])\n    p.add_argument('--dry-run', action='store_true')\n\n    args = p.parse_args(argv)\n    args.staging = args.staging.resolve()\n\n    if not args.compile_db.is_file():\n        print(f\"compile_commands.json not found: {args.compile_db}\", file=sys.stderr)\n        return 2\n\n    # UHT \uc0dd\uc131 \ud5e4\ub354 \uc874\uc7ac \uc5ec\ubd80 \uc0ac\uc804 \uc810\uac80\n    ue_root_hint = find_ue_root_from(args.compile_db.parent) or Path('C:\/UnrealEngine-5.3.2-release')\n    inc_roots = uht_inc_roots(ue_root_hint)\n    has_generated = False\n    for r in inc_roots:\n        if r.is_dir():\n            try:\n                next(r.rglob('*.generated.h'))\n                has_generated = True\n                break\n            except StopIteration:\n                pass\n    if not has_generated:\n        print('[PRECHECK] UHT generated headers not found under any of:')\n        for r in inc_roots:\n            print('  -', r)\n        print('\\n[HINT] Run UHT first to generate headers. Example:')\n        print('  Engine\/Binaries\/DotNET\/UnrealBuildTool\/UnrealBuildTool.exe -Mode=Build '\n              '-Project=\"C:\/UnrealEngine-5.3.2-release\/Samples\/Games\/Lyra\/Lyra.uproject\" '\n              '-Target=\"UnrealEditor Win64 Development\"')\n        return 3\n\n    # \ucef4\ud30c\uc77c \ub370\uc774\ud130 \ub85c\ub4dc\n    data = json.loads(args.compile_db.read_text(encoding='utf-8'))\n    if not isinstance(data, list):\n        print('compile_commands.json must be a list', file=sys.stderr)\n        return 2\n\n    # \ud0c0\uae43 \uc5d4\ud2b8\ub9ac \ud544\ud130\ub9c1(\ud655\uc7a5\uc790 \uae30\uc900)\n    entries: List[dict] = []\n    for e in data:\n        f = e.get('file', '')\n        if not f:\n            continue\n        low = f.lower()\n        if any(low.endswith(ext) for ext in args.filter_ext):\n            entries.append(e)\n\n    print(f\"targets {len(entries)} (staging={args.staging})\")\n    args.staging.mkdir(parents=True, exist_ok=True)\n\n    # \ub4dc\ub77c\uc774\ub7f0: \ucd5c\ub300 50\uac1c\ub9cc \uc694\uc57d \ud504\ub9b0\ud2b8\n    if args.dry_run:\n        for e in entries[:50]:\n            file_path = Path(e.get('file', ''))\n            cmd_str = e.get('command', '')\n            base_dir = Path(e.get('directory', '.'))\n            compiler, rsp_like = parse_compile_command(cmd_str)\n            if compiler is None:\n                compiler = args.msvc\n            rsp_abs = (base_dir \/ rsp_like).resolve() if rsp_like else Path('&lt;inline>')\n            print(f\"[DRY] {file_path}\\n  compiler: {compiler}\\n  rsp_like: {rsp_abs}\")\n        return 0\n\n    # \ubcd1\ub82c \uc2e4\ud589\n    results = []\n    with ThreadPoolExecutor(max_workers=args.max_workers) as ex:\n        futures = [ex.submit(run_translation_for_entry, e, args) for e in entries]\n        for fut in as_completed(futures):\n            results.append(fut.result())\n\n    # \uc694\uc57d \ucd9c\ub825\n    ok = sum(1 for _, rc, _ in results if rc == 0)\n    fail = len(results) - ok\n    print(\"\\n===== SUMMARY =====\")\n    print(f\"OK: {ok}, FAIL: {fail}\")\n    for unit, rc, log in results[:50]:\n        status = 'OK' if rc == 0 else f'FAIL({rc})'\n        print(f\"- {status}: {unit}\\n  {log}\\n\")\n\n    return 0 if fail == 0 else 1\n\n\nif __name__ == '__main__':\n    raise SystemExit(main())<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>1_make_compile_commands_json.bat 2_ue5_fortify_translate.bat ue5_fortify_translate.py<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[66],"tags":[],"class_list":["post-8776","post","type-post","status-publish","format-standard","hentry","category-computing_fortify"],"_links":{"self":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/8776","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=8776"}],"version-history":[{"count":0,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=\/wp\/v2\/posts\/8776\/revisions"}],"wp:attachment":[{"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=8776"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=8776"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hasu0707.duckdns.org\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=8776"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}