fix(nsight-graphics): avoid stale gpu trace summaries

This commit is contained in:
aimidi
2026-04-16 19:20:28 +08:00
parent 7eaf55024c
commit d080b2eb10
9 changed files with 197 additions and 60 deletions
+5 -5
View File
@@ -14,7 +14,7 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software</stron
<p align="center">
<a href="#-quick-start"><img src="https://img.shields.io/badge/Quick_Start-5_min-blue?style=for-the-badge" alt="Quick Start"></a>
<a href="https://hkuds.github.io/CLI-Anything/"><img src="https://img.shields.io/badge/CLI_Hub-Browse_%26_Install-ff69b4?style=for-the-badge" alt="CLI Hub"></a>
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-16_Apps-green?style=for-the-badge" alt="Demos"></a>
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-32_Apps-green?style=for-the-badge" alt="Demos"></a>
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-2%2C162_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License"></a>
</p>
@@ -586,7 +586,7 @@ AI agents are great at reasoning but terrible at using real professional softwar
| 💸 "UI automation breaks constantly" | No screenshots, no clicking, no RPA fragility. Pure command-line reliability with structured interfaces |
| 📊 "Agents need structured data" | Built-in JSON output for seamless agent consumption + human-readable formats for debugging |
| 🔧 "Custom integrations are expensive" | One Claude plugin auto-generates CLIs for ANY codebase through proven 7-phase pipeline |
| ⚡ "Prototype vs Production gap" | 2,162+ tests with real software validation. Battle-tested across 16 major applications |
| ⚡ "Prototype vs Production gap" | 2,162+ tests with real software validation. Battle-tested across 32 major applications |
---
@@ -745,7 +745,7 @@ An agent uses the VideoCaptioner CLI to automatically generate and overlay style
CLI-Anything works on any software with a codebase — no domain restrictions or architectural limitations.
### 🏭 Professional-Grade Testing
Tested across 16 diverse, complex applications spanning creative, productivity, communication, diagramming, AI image generation, AI content generation, network ad blocking, and local LLM inference domains previously inaccessible to AI agents.
Tested across 32 diverse, complex applications spanning creative, productivity, communication, diagramming, AI image generation, AI content generation, network ad blocking, local LLM inference, GPU debugging, and more.
### 🎨 Diverse Domain Coverage
From creative workflows (image editing, 3D modeling, vector graphics) to production tools (audio, office, live streaming, video editing).
@@ -1213,7 +1213,7 @@ HARNESS.md is our definitive SOP for making any software agent-accessible via au
It encodes proven patterns and methodologies refined through automated generation processes.
The playbook distills key insights from successfully building all 16 diverse, production-ready harnesses.
The playbook distills key insights from successfully building all 32 diverse, production-ready harnesses.
### Critical Lessons
@@ -1338,7 +1338,7 @@ MIT License — free to use, modify, and distribute.
**CLI-Anything***Make any software with a codebase Agent-native.*
<sub>A methodology for the age of AI agents | 16 professional software demos | 2,162 passing tests</sub>
<sub>A methodology for the age of AI agents | 32 professional software demos | 2,162 passing tests</sub>
<br>
+16 -7
View File
@@ -9,8 +9,8 @@ CLI-Anything:连接 AI Agent 与全世界软件的桥梁</strong><br>
<p align="center">
<a href="#-快速上手"><img src="https://img.shields.io/badge/快速上手-5_分钟-blue?style=for-the-badge" alt="Quick Start"></a>
<a href="#-实测展示"><img src="https://img.shields.io/badge/Demo-11_款软件-green?style=for-the-badge" alt="Demos"></a>
<a href="#-测试结果"><img src="https://img.shields.io/badge/测试-1%2C508_通过-brightgreen?style=for-the-badge" alt="Tests"></a>
<a href="#-实测展示"><img src="https://img.shields.io/badge/Demo-15_款软件-green?style=for-the-badge" alt="Demos"></a>
<a href="#-测试结果"><img src="https://img.shields.io/badge/测试-1%2C660_通过-brightgreen?style=for-the-badge" alt="Tests"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License"></a>
</p>
@@ -471,7 +471,7 @@ CLI-Anything 适用于任何有代码库的软件 —— 不限领域,不限
### 🏭 专业级测试
在 11 款复杂应用上进行了实测,涵盖创意、生产力、通信、图表AI 内容生成领域 —— 这些软件此前对 AI Agent 来说几乎不可触及。
在 15 款复杂应用上进行了实测,涵盖创意、生产力、通信、图表AI 内容生成和 GPU 调试领域 —— 这些软件此前对 AI Agent 来说几乎不可触及。
### 🎨 覆盖多元领域
@@ -588,12 +588,19 @@ CLI-Anything 适用于任何有代码库的软件 —— 不限领域,不限
<td align="center">✅ 19</td>
</tr>
<tr>
<td align="center"><strong>🟩 <a href="nsight-graphics/agent-harness/">Nsight Graphics CLI</a></strong></td>
<td>GPU 调试与性能分析</td>
<td><code>cli-anything-nsight-graphics</code></td>
<td>官方 ngfx / ngfx-capture 编排 + GPU Trace 摘要</td>
<td align="center">✅ 32</td>
</tr>
<tr>
<td align="center" colspan="4"><strong>合计</strong></td>
<td align="center"><strong>✅ 1,527</strong></td>
<td align="center"><strong>✅ 1,660</strong></td>
</tr>
</table>
> 全部 1,628 项测试 **100% 通过** —— 1,151 项单元测试 + 458 项端到端测试 + 19 项 Node.js 测试。
> 全部 1,660 项测试 **100% 通过** —— 1,182 项单元测试 + 459 项端到端测试 + 19 项 Node.js 测试。
---
@@ -623,8 +630,9 @@ zoom 22 passed ✅ (22 unit + 0 e2e)
drawio 138 passed ✅ (116 unit + 22 e2e)
anygen 50 passed ✅ (40 unit + 10 e2e)
sketch 19 passed ✅ (19 jest, Node.js)
nsight-graphics 32 passed ✅ (31 unit + 1 e2e)
──────────────────────────────────────────────────────────────────────────────
TOTAL 1,628 passed ✅ 100% pass rate
TOTAL 1,660 passed ✅ 100% pass rate
```
---
@@ -691,7 +699,8 @@ cli-anything/
├── 📞 zoom/agent-harness/ # Zoom CLI22 项测试)
├── 📐 drawio/agent-harness/ # Draw.io CLI138 项测试)
├── ✨ anygen/agent-harness/ # AnyGen CLI50 项测试)
── 🎨 sketch/agent-harness/ # Sketch CLI19 项测试,Node.js
── 🎨 sketch/agent-harness/ # Sketch CLI19 项测试,Node.js
└── 🟩 nsight-graphics/agent-harness/ # Nsight Graphics CLI32 项测试)
```
每个 `agent-harness/` 包含一个可安装的 Python 包,位于 `cli_anything.<软件名>/` 下,包含 Click CLI、核心模块、工具类(含 `repl_skin.py` 和后端适配器)以及完整的测试。
+16 -7
View File
@@ -9,8 +9,8 @@ CLI-Anything: AIエージェントと世界のソフトウェアの架け橋</st
<p align="center">
<a href="#-クイックスタート"><img src="https://img.shields.io/badge/Quick_Start-5_min-blue?style=for-the-badge" alt="クイックスタート"></a>
<a href="#-デモンストレーション"><img src="https://img.shields.io/badge/Demos-11_Apps-green?style=for-the-badge" alt="デモ"></a>
<a href="#-テスト結果"><img src="https://img.shields.io/badge/Tests-1%2C508_Passing-brightgreen?style=for-the-badge" alt="テスト"></a>
<a href="#-デモンストレーション"><img src="https://img.shields.io/badge/Demos-12_Apps-green?style=for-the-badge" alt="デモ"></a>
<a href="#-テスト結果"><img src="https://img.shields.io/badge/Tests-1%2C540_Passing-brightgreen?style=for-the-badge" alt="テスト"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="ライセンス"></a>
</p>
@@ -456,7 +456,7 @@ CLI-Anythingは**GUIベースのエージェントアプローチを完全に置
CLI-Anythingはコードベースを持つあらゆるソフトウェアで動作します — ドメインの制限やアーキテクチャの制約はありません。
### 🏭 プロフェッショナルグレードのテスト
クリエイティブ、生産性、コミュニケーション、ダイアグラム、AIコンテンツ生成など、以前はAIエージェントがアクセスできなかった11の多様で複雑なアプリケーションでテスト済み。
クリエイティブ、生産性、コミュニケーション、ダイアグラム、AIコンテンツ生成、GPUデバッグなど、以前はAIエージェントがアクセスできなかった12の多様で複雑なアプリケーションでテスト済み。
### 🎨 多様なドメインカバレッジ
クリエイティブワークフロー(画像編集、3Dモデリング、ベクターグラフィックス)からプロダクションツール(オーディオ、オフィス、ライブストリーミング、動画編集)まで。
@@ -550,12 +550,19 @@ CLI-Anythingはコードベースを持つあらゆるソフトウェアで動
<td align="center">✅ 50</td>
</tr>
<tr>
<td align="center"><strong>🟩 <a href="nsight-graphics/agent-harness/">Nsight Graphics CLI</a></strong></td>
<td>GPUデバッグ & プロファイリング</td>
<td><code>cli-anything-nsight-graphics</code></td>
<td>公式 ngfx / ngfx-capture オーケストレーション + GPU Trace サマリー</td>
<td align="center">✅ 32</td>
</tr>
<tr>
<td align="center" colspan="4"><strong>合計</strong></td>
<td align="center"><strong>✅ 1,508</strong></td>
<td align="center"><strong>✅ 1,540</strong></td>
</tr>
</table>
> **全1,508テストで100%パス** — 1,073ユニットテスト + 435エンドツーエンドテスト。
> **全1,540テストで100%パス** — 1,104ユニットテスト + 436エンドツーエンドテスト。
---
@@ -583,8 +590,9 @@ shotcut 154 passed ✅ (110 unit + 44 e2e)
zoom 22 passed ✅ (22 unit + 0 e2e)
drawio 138 passed ✅ (116 unit + 22 e2e)
anygen 50 passed ✅ (40 unit + 10 e2e)
nsight-graphics 32 passed ✅ (31 unit + 1 e2e)
──────────────────────────────────────────────────────────────────────────────
合計 1,508 passed ✅ 100% パス率
合計 1,540 passed ✅ 100% パス率
```
---
@@ -643,7 +651,8 @@ cli-anything/
├── 🎬 shotcut/agent-harness/ # Shotcut CLI (154テスト)
├── 📞 zoom/agent-harness/ # Zoom CLI (22テスト)
├── 📐 drawio/agent-harness/ # Draw.io CLI (138テスト)
── ✨ anygen/agent-harness/ # AnyGen CLI (50テスト)
── ✨ anygen/agent-harness/ # AnyGen CLI (50テスト)
└── 🟩 nsight-graphics/agent-harness/ # Nsight Graphics CLI (32テスト)
```
`agent-harness/`にはClick CLI、コアモジュール、ユーティリティ(`repl_skin.py`とバックエンドラッパーを含む)、包括的なテストを備えた`cli_anything.<software>/`配下のインストール可能なPythonパッケージが含まれています。
@@ -22,6 +22,8 @@ This harness targets the official command-line interfaces rather than the GUI.
- Frame Debugger capture orchestration
- GPU Trace capture orchestration
- GPU Trace export summarization
- newest-complete GPU Trace export selection when an output root contains
multiple exports
- Generate C++ Capture orchestration
- artifact discovery in output directories
@@ -48,3 +50,7 @@ For one-step performance triage, the intended flow is:
2. optionally pin a version with `--nsight-path`
3. run `gpu-trace capture --auto-export --summarize`
4. inspect the returned summary and the exported `.xls` tables if you need more detail
If the chosen output root already contains older GPU Trace exports, the harness
summarizes the newest complete export directory to avoid mixing stale tables
from earlier runs.
@@ -96,6 +96,11 @@ cli-anything-nsight-graphics gpu-trace summarize ^
--input-dir D:\traces
```
`--input-dir` may point either at a specific exported trace directory or at a
parent output root that contains multiple exports. When multiple complete GPU
Trace exports are present, the CLI summarizes the newest complete export
directory so stale tables are not mixed into the result.
### Generate a C++ capture
```bash
@@ -150,6 +155,8 @@ Capture-producing commands also include:
When `gpu-trace capture --summarize` is used, the result also includes:
- `summary.output_dir`
- `summary.search_root`
- `summary.frame_time_ms`
- `summary.fps_estimate`
- `summary.metrics`
@@ -211,7 +218,8 @@ That gives you:
- the `.ngfx-gputrace` artifact
- exported `FRAME.xls`, `GPUTRACE_FRAME.xls`, and `D3DPERF_EVENTS.xls`
- a parsed summary with frame time, estimated FPS, selected counters, and top GPU events
- a parsed summary from the newest complete export directory, with frame time,
estimated FPS, selected counters, and top GPU events
## Human + AI Workflow
@@ -21,11 +21,64 @@ SUMMARY_METRICS = {
"pcie_throughput_pct": "pcie__throughput.avg.pct_of_peak_sustained_elapsed",
}
REQUIRED_EXPORT_FILES = (
"FRAME.xls",
"GPUTRACE_FRAME.xls",
"D3DPERF_EVENTS.xls",
)
def _find_first(output_dir: str, name: str) -> str | None:
"""Find the first matching file under an export directory."""
matches = sorted(Path(output_dir).rglob(name))
return str(matches[0]) if matches else None
def _find_export_dir(output_dir: str) -> tuple[str, dict[str, str]]:
"""Pick the newest export directory containing a complete GPU Trace export."""
output_root = Path(output_dir).resolve()
matches_by_name = {
name: sorted(output_root.rglob(name))
for name in (*REQUIRED_EXPORT_FILES, "GPUTRACE_REGIMES.xls")
}
candidate_dirs = {
path.parent
for name in REQUIRED_EXPORT_FILES
for path in matches_by_name[name]
}
complete_candidates: list[tuple[int, Path, dict[str, str]]] = []
for directory in candidate_dirs:
required_paths = [directory / name for name in REQUIRED_EXPORT_FILES]
if not all(path.is_file() for path in required_paths):
continue
newest_required_mtime = max(path.stat().st_mtime_ns for path in required_paths)
files = {
"frame": str(directory / "FRAME.xls"),
"trace_frame": str(directory / "GPUTRACE_FRAME.xls"),
"events": str(directory / "D3DPERF_EVENTS.xls"),
"regimes": None,
}
regimes_path = directory / "GPUTRACE_REGIMES.xls"
if regimes_path.is_file():
files["regimes"] = str(regimes_path)
complete_candidates.append((newest_required_mtime, directory, files))
if complete_candidates:
_, export_dir, files = max(
complete_candidates,
key=lambda item: (item[0], str(item[1])),
)
return str(export_dir), files
missing = [
name
for name in REQUIRED_EXPORT_FILES
if not matches_by_name[name]
]
if missing:
raise RuntimeError(
"GPU Trace export summary requires exported tables. Missing: "
+ ", ".join(missing)
)
raise RuntimeError(
"GPU Trace export summary requires FRAME.xls, GPUTRACE_FRAME.xls, and "
"D3DPERF_EVENTS.xls to exist under the same export directory."
)
def _read_kv_file(path: str) -> dict[str, str]:
@@ -124,25 +177,11 @@ def _make_highlights(
def summarize_export_dir(output_dir: str, top_n: int = 10) -> dict[str, Any]:
"""Summarize exported GPU Trace tables from an output directory."""
output_root = str(Path(output_dir).resolve())
frame_path = _find_first(output_root, "FRAME.xls")
trace_frame_path = _find_first(output_root, "GPUTRACE_FRAME.xls")
events_path = _find_first(output_root, "D3DPERF_EVENTS.xls")
regimes_path = _find_first(output_root, "GPUTRACE_REGIMES.xls")
if not frame_path or not trace_frame_path or not events_path:
missing = [
name
for name, path in {
"FRAME.xls": frame_path,
"GPUTRACE_FRAME.xls": trace_frame_path,
"D3DPERF_EVENTS.xls": events_path,
}.items()
if not path
]
raise RuntimeError(
"GPU Trace export summary requires exported tables. Missing: "
+ ", ".join(missing)
)
export_dir, files = _find_export_dir(output_root)
frame_path = files["frame"]
trace_frame_path = files["trace_frame"]
events_path = files["events"]
regimes_path = files["regimes"]
frame_data = _read_kv_file(frame_path)
trace_metrics = _read_kv_file(trace_frame_path)
@@ -180,13 +219,9 @@ def summarize_export_dir(output_dir: str, top_n: int = 10) -> dict[str, Any]:
top_level_events = [item for item in top_events if item["depth"] == 0]
return {
"output_dir": output_root,
"files": {
"frame": frame_path,
"trace_frame": trace_frame_path,
"events": events_path,
"regimes": regimes_path,
},
"output_dir": export_dir,
"search_root": output_root,
"files": files,
"frame_time_ms": frame_time_ms,
"fps_estimate": fps_estimate,
"metrics": summary_metrics,
@@ -2,7 +2,7 @@
## Test Inventory Plan
- `test_core.py`: 31 unit tests planned
- `test_core.py`: 32 unit tests planned
- `test_full_e2e.py`: 4 E2E tests planned
## Unit Test Plan
@@ -19,6 +19,7 @@
- unified and split command construction
- artifact diffing behavior
- exported GPU Trace summary parsing
- newest-complete GPU Trace export selection when multiple exports share an output root
### `core/*.py`
@@ -64,8 +65,8 @@ python -m pytest cli_anything/nsight_graphics/tests -v --tb=no
============================= test session starts =============================
platform win32 -- Python 3.11.9, pytest-9.0.3, pluggy-1.6.0 -- C:\Users\aimidi\AppData\Local\Programs\Python\Python311\python.exe
cachedir: .pytest_cache
rootdir: D:\code\D5\CLI-Anything-nsight-graphics-dev\nsight-graphics\agent-harness
collecting ... collected 35 items
rootdir: C:\Users\aimidi\.codex\worktrees\da29\CLI-Anything\nsight-graphics\agent-harness
collecting ... collected 36 items
cli_anything/nsight_graphics/tests/test_core.py::TestOutputAndErrors::test_output_json PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestOutputAndErrors::test_handle_error_debug PASSED
@@ -82,6 +83,7 @@ cli_anything/nsight_graphics/tests/test_core.py::TestCommandBuilders::test_build
cli_anything/nsight_graphics/tests/test_core.py::TestCommandBuilders::test_build_split_capture_command_maps_wait_seconds PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCommandBuilders::test_diff_snapshots_reports_new_nonempty_files PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCommandBuilders::test_gpu_trace_summary_from_export_dir PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCommandBuilders::test_gpu_trace_summary_prefers_newest_complete_export_dir PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCoreModules::test_frame_capture_uses_unified_ngfx PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCoreModules::test_frame_capture_split_mode_rejects_perf_exports PASSED
cli_anything/nsight_graphics/tests/test_core.py::TestCoreModules::test_gpu_trace_requires_arch_for_metric_set PASSED
@@ -103,13 +105,13 @@ cli_anything/nsight_graphics/tests/test_full_e2e.py::TestTargetedE2E::test_frame
cli_anything/nsight_graphics/tests/test_full_e2e.py::TestTargetedE2E::test_gpu_trace_capture SKIPPED
cli_anything/nsight_graphics/tests/test_full_e2e.py::TestTargetedE2E::test_cpp_capture SKIPPED
======================== 32 passed, 3 skipped in 1.92s ========================
======================== 33 passed, 3 skipped in 1.86s ========================
```
## Summary Statistics
- Total tests collected: 35
- Passed: 32
- Total tests collected: 36
- Passed: 33
- Skipped: 3
- Pass rate for executed tests: 100%
@@ -118,3 +120,5 @@ cli_anything/nsight_graphics/tests/test_full_e2e.py::TestTargetedE2E::test_cpp_c
- `doctor info` E2E passed against the local Nsight Graphics installation.
- Target-dependent E2E scenarios are implemented but currently skipped until
`NSIGHT_GRAPHICS_TEST_EXE` (and optional args/workdir) are provided.
- GPU Trace summary coverage now includes the case where an output root contains
multiple export directories; the newest complete export is selected.
@@ -172,7 +172,8 @@ class TestBackendDiscovery:
assert result["installations"][0]["tool_mode"] == "registered-only"
def test_list_installations_merges_registry_metadata_into_filesystem_entry(self, tmp_path):
install_dir = tmp_path / "Nsight Graphics 2025.1" / "host" / "windows-desktop-nomad-x64"
install_root = tmp_path / "Nsight Graphics 2025.1"
install_dir = install_root / "host" / "windows-desktop-nomad-x64"
install_dir.mkdir(parents=True)
(install_dir / "ngfx.exe").write_text("", encoding="utf-8")
@@ -180,7 +181,7 @@ class TestBackendDiscovery:
{
"display_name": "NVIDIA Nsight Graphics 2025.1",
"display_version": "25.1.0",
"install_location": str(install_dir),
"install_location": str(install_root),
"install_source": "C:/Installers",
"uninstall_string": "msiexec /x ...",
"publisher": "NVIDIA Corporation",
@@ -322,6 +323,58 @@ class TestCommandBuilders:
assert summary["top_events"][1]["event"] == "DirectLighting"
assert summary["highlights"]
def test_gpu_trace_summary_prefers_newest_complete_export_dir(self, tmp_path):
old_export = tmp_path / "A_old_export"
new_export = tmp_path / "B_new_export"
old_export.mkdir()
new_export.mkdir()
old_files = {
"frame": old_export / "FRAME.xls",
"trace": old_export / "GPUTRACE_FRAME.xls",
"events": old_export / "D3DPERF_EVENTS.xls",
}
old_files["frame"].write_text("GPU frame time\t40.0\n", encoding="utf-8")
old_files["trace"].write_text(
"FE_B.TriageAC.fe__draw_count.sum\t10\n",
encoding="utf-8",
)
old_files["events"].write_text(
"event_text\ttime_ms\nFrame 1\t40.0\nOldPass\t30.0\n",
encoding="utf-8",
)
new_files = {
"frame": new_export / "FRAME.xls",
"trace": new_export / "GPUTRACE_FRAME.xls",
"events": new_export / "D3DPERF_EVENTS.xls",
}
new_files["frame"].write_text("GPU frame time\t12.5\n", encoding="utf-8")
new_files["trace"].write_text(
"FE_B.TriageAC.fe__draw_count.sum\t123\n",
encoding="utf-8",
)
new_files["events"].write_text(
"event_text\ttime_ms\nFrame 2\t12.5\nNewPass\t8.5\n",
encoding="utf-8",
)
for path in old_files.values():
os.utime(path, ns=(1_000_000_000, 1_000_000_000))
for path in new_files.values():
os.utime(path, ns=(2_000_000_000, 2_000_000_000))
summary = gpu_trace.summarize_export_dir(str(tmp_path), top_n=3)
assert summary["output_dir"] == str(new_export.resolve())
assert summary["search_root"] == str(tmp_path.resolve())
assert summary["frame_time_ms"] == pytest.approx(12.5)
assert summary["metrics"]["draw_count"] == 123
assert summary["top_events"][0]["event"] == "NewPass"
assert Path(summary["files"]["frame"]).parent == new_export.resolve()
assert Path(summary["files"]["trace_frame"]).parent == new_export.resolve()
assert Path(summary["files"]["events"]).parent == new_export.resolve()
class TestCoreModules:
@patch("cli_anything.nsight_graphics.core.frame.backend.run_with_artifacts")
@@ -50,6 +50,12 @@ def _dedupe(values: Iterable[str]) -> list[str]:
return result
def _normalized_install_key(path: str) -> str:
"""Normalize a path for install-root comparisons across slash styles."""
normalized = os.path.normcase(os.path.normpath(path))
return re.sub(r"[\\/]+", "/", normalized).rstrip("/")
def _scan_directory(directory: str) -> dict[str, str]:
"""Scan a directory for known Nsight binaries."""
found: dict[str, str] = {}
@@ -331,7 +337,7 @@ def list_installations(
install_location = record.get("install_location")
normalized_location = None
if install_location:
normalized_location = os.path.normcase(os.path.normpath(install_location))
normalized_location = _normalized_install_key(install_location)
guessed_version = (
_extract_version_from_text(record.get("display_name") or "")
or _extract_version_from_text(record.get("display_version") or "")
@@ -341,7 +347,14 @@ def list_installations(
if normalized_location:
for item in installations:
item_root = item.get("install_root")
if item_root and os.path.normcase(os.path.normpath(item_root)) == normalized_location:
if not item_root:
continue
normalized_root = _normalized_install_key(item_root)
if (
normalized_root == normalized_location
or normalized_root.startswith(normalized_location + "/")
or normalized_location.startswith(normalized_root + "/")
):
matched_entry = item
break
elif guessed_version: