diff --git a/README.md b/README.md index d6d3de6..533a87c 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,16 @@ pip install -e . agentdecompile-cli --server-url http://YOUR_SERVER:8080/ tool --list-tools ``` +### Optional: .NET/IL decompilation support (ilspycmd) + +`pipx install agentdecompile[all]` (or `pip install -e .[all]` from source) installs every optional **Python** feature, but .NET/IL assembly decompilation depends on [ILSpy's](https://github.com/icsharpcode/ILSpy) `ilspycmd`, which is a .NET global tool, not a PyPI package — it cannot be expressed as a pip dependency and is not bundled by any extras group. Install it separately: + +```bash +dotnet tool install -g ilspycmd +``` + +Requires the [.NET SDK](https://dotnet.microsoft.com/download). + ### Option 3: Docker (run the server) **Published image (no build required):** diff --git a/pyproject.toml b/pyproject.toml index 8ac801c..3d54059 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,13 @@ test = [ "pytest-sugar>=1.0.0", "ghidra-stubs>=12.0.4", ] +# Bundles every pip-installable optional feature. Does NOT include `ilspycmd` +# (.NET/IL decompilation support) -- that's ILSpy's dotnet global tool, not a +# PyPI package, and cannot be expressed as a pip dependency. Install it +# separately with `dotnet tool install -g ilspycmd`; see README.md. +all = [ + "agentdecompile[semantic]", +] [build-system] requires = [ diff --git a/tests/test_rewrite_queue.py b/tests/test_rewrite_queue.py index bc7d530..5cbe6e1 100644 --- a/tests/test_rewrite_queue.py +++ b/tests/test_rewrite_queue.py @@ -23,6 +23,25 @@ def _claim_worker(work_dir: str, request_id: str, claimant: str, result_path: st Path(result_path).write_text("1" if ok else "0", encoding="utf-8") +def _write_request_worker(work_dir: str, name: str) -> None: + rewrite_queue.write_rewrite_request( + Path(work_dir), function_name=name, entry="0x1", candidate_source=f"src-{name}", mismatch_class=None, mismatch_histogram=None + ) + + +# macOS defaults multiprocessing to the "spawn" start method, which re-imports +# the target function in a fresh interpreter rather than fork()ing the +# already-loaded parent. Under pytest, the test module isn't reliably +# importable by that fresh interpreter (no guaranteed `tests` package on +# sys.path), so spawn-based Process creation fails here with +# ModuleNotFoundError/AttributeError on macOS CI even though the exact same +# code passes on Linux (which defaults to fork). Force fork explicitly -- +# available on both Linux and macOS (the only two CI platforms) -- since +# these tests only need process-level isolation, not spawn's clean-slate +# import behavior. +_FORK_CONTEXT = multiprocessing.get_context("fork") + + def test_write_rewrite_request_creates_pending_entry(tmp_path: Path) -> None: request_id = rewrite_queue.write_rewrite_request( tmp_path, @@ -241,8 +260,8 @@ def test_concurrent_claims_from_separate_processes_only_one_wins(tmp_path: Path) ) result_a = tmp_path / "result_a.txt" result_b = tmp_path / "result_b.txt" - proc_a = multiprocessing.Process(target=_claim_worker, args=(str(tmp_path), request_id, "proc-a", str(result_a))) - proc_b = multiprocessing.Process(target=_claim_worker, args=(str(tmp_path), request_id, "proc-b", str(result_b))) + proc_a = _FORK_CONTEXT.Process(target=_claim_worker, args=(str(tmp_path), request_id, "proc-a", str(result_a))) + proc_b = _FORK_CONTEXT.Process(target=_claim_worker, args=(str(tmp_path), request_id, "proc-b", str(result_b))) proc_a.start() proc_b.start() proc_a.join(timeout=10) @@ -264,13 +283,8 @@ def test_write_rewrite_request_survives_concurrent_writes_to_different_entries(t not lose each other's entries (the lock serializes the whole file, not just same-entry races).""" - def _writer(work_dir: str, name: str) -> None: - rewrite_queue.write_rewrite_request( - Path(work_dir), function_name=name, entry="0x1", candidate_source=f"src-{name}", mismatch_class=None, mismatch_histogram=None - ) - procs = [ - multiprocessing.Process(target=_writer, args=(str(tmp_path), f"sub_{i}")) + _FORK_CONTEXT.Process(target=_write_request_worker, args=(str(tmp_path), f"sub_{i}")) for i in range(6) ] for proc in procs: