Metadata-Version: 2.4
Name: pywv2
Version: 2.12
Summary: Python bindings for WebView2 — a pure-C proxy layer (99 fully implemented APIs) + ctypes, supporting standalone windows and PyQt5 integration
Author-email: "Dvs (DvsXT)" <dvs6666@163.com>
License: MIT License
        
        Copyright (c) 2026 Dvs (DvsXT)
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
        ---
        
        This product includes software developed by Microsoft (WebView2 SDK).
        See https://learn.microsoft.com/microsoft-edge/webview2/ for license terms.
        
Project-URL: Homepage, https://gitcode.com/dvsxt
Project-URL: Source, https://gitcode.com/dvsxt/webview2-py
Project-URL: Changelog, https://gitcode.com/dvsxt/pywv2/blob/main/CHANGELOG.md
Keywords: webview2,browser,chromium,edge,gui,windows,pyqt5,ctypes,embed-browser,microsoft-edge
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: C
Classifier: Topic :: Software Development :: User Interfaces
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: qt
Requires-Dist: PyQt5>=5.15; extra == "qt"
Provides-Extra: qt6
Requires-Dist: PySide6>=6.5; extra == "qt6"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build>=0.10; extra == "dev"
Requires-Dist: twine>=4; extra == "dev"
Dynamic: license-file

# pywv2

> **Python bindings for WebView2** — a pure-C proxy layer (all 99 APIs truly implemented) + ctypes wrapper.
> Supports **standalone window mode** and **PyQt5 integration**; the window belongs to the calling process.
> **Three native backends, one naming scheme** — `wv2_<cc>_<arch>.dll`;
> the two 64-bit DLLs ship in the wheel, the 32-bit GCC build is an optional add-on.
> WebView2Loader is embedded, so no extra files are needed.

[![Python](https://img.shields.io/badge/python-3.8%2B-blue)]()
[![Platform](https://img.shields.io/badge/platform-Windows-blue)]()
[![Version](https://img.shields.io/badge/version-2.12-green)]()
[![License](https://img.shields.io/badge/license-MIT-green)]()

---

## ✨ Features

| Feature | Description |
|---|---|
| 🎯 **99 APIs** | Cookies / downloads / permissions / request filtering / printing / screenshots / CDP / DevTools / events … **zero degraded stubs** |
| 📦 **Self-contained** | `WebView2Loader.dll` is embedded inside every proxy-layer DLL and released at runtime |
| 🎛️ **3 native backends** | One naming scheme — `wv2_<cc>_<arch>.dll`: x64 TCC + x64 GCC ship in the wheel, x86 GCC is optional |
| 🧩 **Dual compiler** | One set of C sources compiles with both GCC and TCC (identical symbols) |
| 🪟 **Standard integration** | The window is created standalone and **belongs to the calling process/main thread** — no separate window process |
| 🐍 **Pythonic wrapper** | Context managers, event decorators, an exception hierarchy, type hints |
| 🖼️ **PyQt5 support** | Bind with a single line: `QtWebView(win)`; the size follows automatically |
| ⚡ **Synchronous calls** | Asynchronous APIs pump messages internally and block, so they look synchronous |
| 🚫 **No spill-over** | In-page navigation / `target=_blank` / `window.open` all stay in the current window, never popping up system Edge |
| 🧹 **Hides Edge traces** | DevTools / context menu / accelerators / status bar / zoom control are all off by default |
| 🔗 **js_api bridge** | Pass `js_api=obj` and every public method becomes `window.pywv2.api.<method>(...)`, returning a **Promise** |
| 🧱 **DOM access** | `wv.dom` / `Element` — query, read & write text / attributes / style / classes, traverse, mutate, subscribe to events |
| 🔄 **Shared state** | `wv.state` — two-way observable key/value store, re-published automatically after navigation |
| 🔐 **CSRF token** | Per-session `wv.token`, mirrored to the page as `window.pywv2.token` |
| ✅ **Ready event** | `pywv2ready` (and `window.pywv2.on("ready", …)`) fires once the JS shim is installed |

---

## 📦 Installation

```bash
pip install pywv2
```

Requirements: **Windows 10/11** + the **WebView2 Runtime** (usually preinstalled).

A normal `pip install` gives you the two 64-bit backends:

```
pywv2/bin/
├─ wv2_tcc_x64.dll    ← default backend, x64 (TCC)   ~260 KB
└─ wv2_gcc_x64.dll    ← x64 (GCC, -O2)               ~357 KB
```

**`WebView2Loader.dll` is not needed** — it is embedded as a resource inside both DLLs.

### 32-bit support (optional)

The wheel is tagged `win_amd64` and ships the 64-bit backends only. 32-bit (`win32`)
interpreters are still supported: the third backend, `wv2_gcc_x86.dll`
(x86 / i386, GCC, ~472 KB), is **not** bundled into the wheel (it would add ~470 KB
for a small minority of users), but it **is** shipped in the source distribution
(`sdist`) as `pywv2/bin_x86/wv2_gcc_x86.dll`.

**Step 1 — get the DLL.** Either take it from a source checkout / unpacked sdist
(`MANIFEST.in` includes it):

```
pywv2/bin_x86/wv2_gcc_x86.dll
```

…or build it from source yourself — see **Step 3** below / [Building from source](#️-building-from-source).

**Step 2 — copy it into the installed package's `bin/` directory:**

```python
import os, shutil, pywv2

pkg = os.path.dirname(pywv2.__file__)          # ...\site-packages\pywv2
shutil.copy2(r"<your source tree>\pywv2\bin_x86\wv2_gcc_x86.dll",
             os.path.join(pkg, "bin", "wv2_gcc_x86.dll"))
```

**Step 3 — (alternative) build it from source instead of copying it.** In a source
checkout this produces `<source tree>\pywv2\bin\wv2_gcc_x86.dll`, which you then copy
as shown in Step 2 (or install the whole tree with `pip install -e .`):

```bash
python -m pywv2.scripts.build_native --arch x86
```

**Step 4 — nothing else to configure.** Once the file is in `bin/` (or left in
`bin_x86/`), a **32-bit interpreter picks it up automatically**: the loader calls
`host_arch()` and selects `gcc` for `x86`. No environment variable, no config file.

> ⚠️ **32-bit is GCC-only.** TCC's 32-bit runtime is not reliable for this codebase,
> so `WebView(backend="tcc")` on a 32-bit interpreter raises
> **`PYWV2_ERR_UNSUPPORTED`**.

---

## 🧬 Backends and architectures

Three proxy-layer DLLs can be produced. They all follow the same naming scheme
`wv2_<cc>_<arch>.dll` (compiler + architecture spelled out in the file name):

| File | Arch | Compiler | Size | In the default wheel? |
|---|---|---|---|---|
| `wv2_tcc_x64.dll` | x86_64 | TCC | ~260 KB | ✅ yes — **default backend** |
| `wv2_gcc_x64.dll` | x86_64 | GCC (-O2) | ~357 KB | ✅ yes |
| `wv2_gcc_x86.dll` | i386 | GCC (-O2) | ~472 KB | ❌ **no** — optional, lives in the sdist as `bin_x86/` |

Directory layout in the source tree / sdist:

```
pywv2/
├─ bin/          ← 64-bit backends, packaged into the wheel
│  ├─ wv2_tcc_x64.dll
│  └─ wv2_gcc_x64.dll
└─ bin_x86/      ← optional 32-bit backend, NOT packaged into the wheel
   └─ wv2_gcc_x86.dll
```

### Automatic selection

```python
import pywv2

pywv2.host_arch()      # -> "x64" or "x86"  (architecture of the running interpreter)
pywv2.ARCH_BACKENDS    # -> {"x64": ("tcc", "gcc"), "x86": ("gcc",)}
pywv2.BACKENDS         # -> {"tcc": "wv2_tcc_x64.dll", "gcc": "wv2_gcc_x64.dll"}
pywv2.DEFAULT_BACKEND  # -> "tcc"
```

| Interpreter | Default backend | DLL used |
|---|---|---|
| 64-bit Python | `tcc` | `wv2_tcc_x64.dll` |
| 32-bit Python | `gcc` | `wv2_gcc_x86.dll` (must be placed in `bin/` or `bin_x86/` — see above) |

Explicit override:

```python
wv = WebView()                                # automatic (tcc on x64, gcc on x86)
wv = WebView(backend="gcc")                   # force the GCC build
wv = WebView(backend=r"D:\my\custom_wv2.dll") # explicit path / file name
```

### DLL search order

When no explicit path is given, the loader looks for the proxy-layer DLL in this order:

```
1. the explicit path / file name given via backend=...
2. bin/     — the name matching the current architecture: wv2_<backend>_<arch>.dll
3. bin_x86/ — the matching name (only when arch == "x86")
4. bin/     — any wv2_*_<arch>.dll
5. bin/     — any wv2_*.dll   (compatibility with legacy un-suffixed names)
```

> ℹ️ This is the lookup for the **proxy-layer DLL**. The lookup for
> `WebView2Loader.dll` inside it is a different mechanism — see
> [WebView2Loader lookup strategy](#webview2loader-lookup-strategy-4-levels-no-extra-files).

### Wheel platform tag

```
pywv2-2.12-py3-none-win_amd64.whl
              │   │    └─ platform : 64-bit Windows (pinned by bdist_wheel in setup.py)
              │   └────── abi      : none (ctypes only, no CPython C-API)
              └────────── python   : Python 3
```

* **Why `win_amd64` instead of `any`** — the package contains native Windows
  binaries, and pip only performs *platform* checks when installing. Tagging it
  `any` ("pure Python") would let Linux/macOS attempt the install and only fail later.
* **Why the ABI tag is `none`** — nothing links against the CPython C-API; all native
  interop goes through `ctypes`, which is ABI-stable.

---

## 🚀 Quick start

### Standalone window mode (default)

```python
from pywv2 import WebView

# default TCC build; for the GCC-optimized build use WebView(backend="gcc")
with WebView(title="My App", size=(1200, 800)) as wv:
    wv.navigate("https://example.com")
    wv.wait_loaded()

    print("Title:", wv.title)
    print("URL:", wv.url)
    print("Version:", wv.version)

    # Run JS and get the result back (decoded from JSON automatically)
    print("Links:", wv.eval("document.querySelectorAll('a').length"))

    wv.screenshot("shot.png")
    wv.print_pdf("page.pdf")

    while not wv.should_close:
        wv.pump(100)      # pump messages to keep the window responsive
```

> 🔎 Backend / architecture selection (including the optional 32-bit build): see
> [Backends and architectures](#-backends-and-architectures).

### PyQt5 integration

```python
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from pywv2.qt import QtWebView

app = QApplication([])
win = QMainWindow()
win.resize(1200, 800)

wv = QtWebView(win)              # bind to the Qt window
wv.navigate("https://example.com")
wv.wait_loaded()

win.show()
app.exec_()
```

---

## 📚 Feature examples

### Event callbacks

```python
with WebView() as wv:
    @wv.on("navigation_completed")
    def _(data, user):
        print("loaded:", data["isSuccess"])

    @wv.on("title_changed")
    def _(data, user):
        print("title changed:", data["title"])

    wv.navigate("https://example.com")
    wv.pump(3000)
```

> ⚠️ **Always use `wv.pump(ms)` instead of `time.sleep()`** — WebView2 events are
> dispatched by the Windows message loop, so callbacks do not run while sleeping.

### Cookie management

```python
wv.set_cookie("token", "abc123", domain="example.com")
for c in wv.cookies("https://example.com"):
    print(c.name, c.value, c.domain)

wv.delete_cookie("token", "https://example.com")
wv.clear_cookies()
wv.clear_browsing_data()
```

### Permission control

```python
from pywv2 import PERM_CAMERA, PERM_STATE_ALLOW, PERM_STATE_DENY

wv.set_permission(PERM_CAMERA, PERM_STATE_ALLOW)
wv.set_permission(PERM_CAMERA, PERM_STATE_DENY, origin="https://evil.com")
```

### Request interception / local asset mapping

```python
fid = wv.add_request_filter("*")
rid = wv.add_response_filter("*")
# ...
wv.remove_request_filter(fid)
wv.remove_response_filter(rid)

wv.map_virtual_host("app.local", "./dist")
wv.navigate("https://app.local/index.html")
```

### DevTools protocol

```python
info = wv.cdp("Browser.getVersion")
print(info["product"])

html = wv.cdp("Runtime.evaluate",
              {"expression": "document.documentElement.outerHTML"})
```

---

## 🌉 JavaScript ↔ Python

v2.12 adds a two-way bridge modelled on pywebview's `js_api`. Pass an object as
`js_api`; every **public** method (names not starting with `_`) becomes callable
from the page as `window.pywv2.api.<method>(...)`, which returns a **Promise**.

```python
from pywv2 import WebView

class Api:
    def add(self, a, b):
        return a + b
    def greet(self, name):
        return f"hello {name}"

with WebView(js_api=Api()) as wv:
    wv.load_html("<html><body></body></html>")
    wv.wait_loaded()
```

```javascript
// Promise-based, on the page
const sum = await window.pywv2.api.add(2, 3);        // 5
window.pywv2.api.greet("world").then(v => console.log(v));
```

Expose more objects at runtime, optionally under a namespace:

```python
wv.js.expose(Calculator())                 # -> window.pywv2.api.<method>
wv.js.expose(Calculator(), name="calc")    # -> window.pywv2.api.calc.<method>
wv.js.unexpose("calc")                     # remove it again
wv.js.methods                              # -> ['add', 'greet', ...]
```

Rules and limits:

* only public methods are exposed (`_private` is skipped);
* return values must be JSON-serialisable (`int` / `float` / `bool` / `str` /
  `None` / `list` / `dict`) — anything else is coerced with `repr()`;
* a Python exception **rejects** the JS Promise: `err.code` carries a
  `PYWV2_ERR_*` code, `err.message` the text and `err.suggestion` the hint;
* unknown method → `PYWV2_ERR_INVALID_ARG`; wrong arguments →
  `PYWV2_ERR_INVALID_ARG`; an exception raised inside the method →
  `PYWV2_ERR_INTERNAL`.

### Calling JavaScript from Python

```python
wv.js.run_js("document.body.style.background = 'red';")   # fire and forget
val = wv.js.evaluate_js("1 + 2")                            # -> 3
obj = wv.js.evaluate_js("({a: 1, b: [2, 3]})")              # -> {'a': 1, 'b': [2, 3]}
wv.js.dispatch("my-event", {"k": "v"})                      # deliver a named event
```

```javascript
window.pywv2.on("my-event", detail => console.log(detail));
```

### Ready event

```javascript
window.addEventListener("pywv2ready", () => { /* the shim is installed */ });
// equivalent:
window.pywv2.on("ready", () => { /* ... */ });
```

See [docs/bridge.md](docs/bridge.md) for the wire protocol, the CSRF token and
the shared-state sync semantics.

---

## 🧱 DOM access

`wv.dom` exposes the page's DOM to Python (mirrors `webview.dom`); every
operation is a single JavaScript round-trip.

```python
el   = wv.dom.get_element("#login")          # Element or None
els  = wv.dom.get_elements(".item")          # [Element, ...]
body = wv.dom.body                           # the <body> element
new  = wv.dom.create_element("<b>hi</b>")    # inserted; returns the new Element
```

Reading and writing:

```python
el.text                    # textContent   (get / set)
el.html                    # innerHTML     (get / set)
el.value                   # form value    (get / set)
el.attributes["data-id"] = "7"       # dict-like; assign None to delete
el.style["width"] = "100px"          # dict-like
el.classes.add("active")             # list-like: add / remove / toggle / contains / clear / to_list
el.visible, el.focused               # booleans
```

Interaction and traversal:

```python
el.click(); el.focus(); el.blur()
el.parent, el.next, el.previous, el.children

el.append("<span>x</span>", mode=ManipulationMode.LastChild)
el.copy(target, mode=ManipulationMode.Replace, id="copy-1")
el.move(target); el.remove(); el.empty()
```

`ManipulationMode`: `LastChild` (default) / `FirstChild` / `Before` / `After` /
`Replace`.

Events are handled on the Python side:

```python
btn = wv.dom.get_element("#btn")
btn.events.click += on_click          # or: btn.on("click", on_click)

def on_click(detail):
    print(detail)   # {'type': 'click', 'key': None, 'target': 'button', 'value': ''}
```

Control the browser's default action and debounce deliveries:

```python
from pywv2 import DOMEventHandler

el.events.click += DOMEventHandler(on_click, prevent_default=True,
                                   stop_propagation=True, debounce=200)
```

---

## 🔄 Shared state

`wv.state` is a two-way key/value store that stays in sync between Python and the
page (mirrors pywebview's `window.state`). Dict-style and attribute-style access
are both supported.

```python
wv = WebView(state={"theme": "dark"})

wv.state["count"] = 7            # mapping style  -> pushes to the page
wv.state.user = "alice"          # attribute style
print(wv.state.count)            # read
del wv.state["count"]            # delete           -> pushes the deletion

wv.state.on_change(lambda c: print(c.event, c.key, c.value))
wv.state.resync()                # force a re-publish (also runs after each load)
wv.state.to_dict()               # snapshot
```

```javascript
pywv2.state.theme = "light";                 // -> syncs to Python
pywv2.state.addEventListener("change", e => console.log(e.detail));  // {key, value}
pywv2.state.addEventListener("delete", e => console.log(e.detail));
delete pywv2.state.theme;                    // -> syncs the deletion
```

Semantics and pitfalls:

* state is **per window** and is **re-published automatically after navigation**
  — Python keeps the authoritative copy, so the page always comes back in sync;
* values must be JSON-serialisable (create the store with the `state=` argument
  to seed it);
* **mutating a nested object is not detected** — `wv.state["user"]["name"] = "x"`
  has no effect; assign the whole value instead:
  `wv.state["user"] = {"name": "x"}`.

---

## 🔧 API quick reference

**Lifecycle**: `start()` `close()` `pump(ms)` `wait(ms)` `wait_loaded(timeout)`
`ready` `hwnd` `should_close`

**Navigation**: `navigate(url)` `go_back()` `go_forward()` `reload()` `stop()`
`load_html(html)` `url` `title` `can_go_back` `can_go_forward`

**Scripts**: `exec_script(js)` `eval(js)` `add_init_script(js)`
`remove_init_script(id)` `post_message(msg)`

**Settings**: `set(**kwargs)` `user_agent` `zoom`

**Window**: `set_bounds(x,y,w,h)` `show()` `focus()` `minimize()` `maximize()`
`restore()` `fullscreen()` `on_top()` `move(x,y)` `resize(w,h)` `set_title(t)`

**Cookies**: `cookies(url)` `set_cookie(...)` `delete_cookie(...)`
`clear_cookies()` `clear_browsing_data(kinds)`

**Permissions / filters**: `set_permission(kind, state, origin=None)`
`add_request_filter()` `remove_request_filter()` `add_response_filter()`
`remove_response_filter()` `map_virtual_host()` `clear_virtual_host()`

**Print / screenshot / DevTools**: `print_pdf(path)` `show_print_ui()`
`screenshot(path)` `screenshot_b64()` `open_devtools()` `close_devtools()`
`cdp(method, params)`

**Downloads**: `download_cancel(id)` `download_pause(id)` `download_resume(id)`
`download_state(id)` `download_path(id)`

**JS bridge** (`wv.js`): `expose(obj, name=None)` `unexpose(name)` `methods` `run_js(js)` `evaluate_js(js)` `dispatch(event, payload)` `on_event(name, cb)` `off_event(name, cb)` `wait_ready(timeout)` `ready`

**DOM** (`wv.dom`): `get_element(sel)` `get_elements(sel)` `body` `document` `create_element(html, parent=None, mode=)` — `Element`: `tag` `id` `path` `text` `html` `value` `attributes` `style` `classes` `visible` `focused` `show()` `hide()` `toggle()` `click()` `focus()` `blur()` `parent` `next` `previous` `children` `append()` `copy()` `move()` `remove()` `empty()` `on()` `off()` `events` (`+=` / `-=`)

**Shared state** (`wv.state`): `__getitem__` / `__setitem__` / `__delitem__` `get()` `update(**kw)` `clear()` `to_dict()` `keys()` `items()` `on_change()` `off_change()` `resync()`

**Security / debugging**: `wv.token` `WebView(debug_bridge=True)` `wv._bridge_errors`

**Events**: `on(event, handler)` `off(event, handler)`

Event list: `navigation_starting` `navigation_completed` `source_changed`
`title_changed` `new_window_requested` `window_close_requested`
`history_changed` `content_loading` `dom_content_loaded` `web_message_received`
`process_failed` `fullscreen_changed` `download_starting` `permission_requested`

---

## 🏗️ Architecture

```
Your process (Python)
├─ Main thread + message loop (required by WebView2)
├─ Main window (created standalone, owned by this process) — class Wv2HostWindowClass
│   └─ WebView2 controller (put_ParentWindow)
└─ msedgewebview2.exe child process (Chromium engine, managed by Microsoft)
    ├─ browser process
    ├─ renderer process
    └─ GPU process
```

**Why must the window live in the calling process?**
WebView2's `ICoreWebView2Controller` requires same-process, same-thread (STA) access.
Creating it across processes leads to failed COM calls and a broken message loop.

### No spill-over (new-window request handling)

By default, `target="_blank"` / `window.open()` / form `target=_blank`
are **all folded into the current window** and never open system Edge:

```python
# No configuration needed — works out of the box
with WebView() as wv:
    wv.navigate("https://example.com")
    # any new-window request from the page opens in this window
```

How it works (pure native, no JS injection): inside the `NewWindowRequested`
callback we **synchronously** call `put_NewWindow(current webview)` +
`put_Handled(TRUE)`; navigation goes through a deferred queue (you cannot call
`Navigate` synchronously from inside the callback).

### Hiding Edge traces

All built-in Edge UI is off by default and can be enabled on demand:

```python
# At construction time
wv = WebView(devtools=True, context_menu=True)

# At runtime
wv.set_chrome_ui(devtools=True)      # temporarily enable DevTools for debugging
wv.set_chrome_ui(context_menu=False) # turn the context menu off
```

| Item | Default |
|---|---|
| DevTools (F12 / right-click "Inspect") | off |
| Edge built-in context menu | off |
| Browser accelerators (Ctrl+R/F5/Ctrl+P) | off |
| Status bar (link hint in the lower-left) | off |
| Zoom control | off |

### WebView2Loader lookup strategy (4 levels, no extra files)

The proxy-layer DLL embeds `WebView2Loader.dll` and looks it up at runtime in this order:

1. `PATH` / current directory
2. the proxy-layer DLL's own directory
3. **the Edge WebView2 Runtime install directory** (registry `pv` value + common-path scan)
4. **released from the embedded resource** into `%TEMP%\pywv2_<pid>\` and loaded

So: **on most machines not a single extra file is needed**, and even in extreme
cases it is self-contained.

> ℹ️ This is the lookup for **`WebView2Loader.dll`** *inside* the proxy-layer DLL.
> Finding the proxy-layer DLL itself (`wv2_<cc>_<arch>.dll`) is a separate
> mechanism — see [DLL search order](#dll-search-order).

---

## 🛠️ Building from source

```bash
# Build every target: x64 TCC + x64 GCC + x86 GCC
python -m pywv2.scripts.build_native

# One architecture only
python -m pywv2.scripts.build_native --arch x64
python -m pywv2.scripts.build_native --arch x86      # needs an i686 toolchain (see below)

# One compiler only (TCC is x64-only)
python -m pywv2.scripts.build_native --cc tcc
python -m pywv2.scripts.build_native --cc gcc --arch x64

# List the build targets and exit
python -m pywv2.scripts.build_native --list

# Do not embed the loader (~160 KB smaller per DLL; you must then supply WebView2Loader.dll)
python -m pywv2.scripts.build_native --no-embed

# Package
python -m build --wheel     # -> dist/pywv2-<ver>-py3-none-win_amd64.whl   (bin/ only)
python -m build --sdist     # -> dist/pywv2-<ver>.tar.gz                  (includes bin_x86/)
```

### Build targets

Artifacts land in `pywv2/bin/` and are named `wv2_<cc>_<arch>.dll`:

| Target | Compiler | Output |
|---|---|---|
| `tcc_x64` | TCC (`D:\winlibs\tcc\tcc.exe` by default) | `bin/wv2_tcc_x64.dll` |
| `gcc_x64` | MinGW-w64 x64 (`D:\winlibs\mingw64\bin\gcc.exe`) | `bin/wv2_gcc_x64.dll` |
| `gcc_x86` | MinGW-w64 i686 (`D:\msys64\mingw32\bin\gcc.exe`) | `bin/wv2_gcc_x86.dll` |

Every artifact is verified by reading the PE `Machine` field, so the printed
architecture can be trusted.

### Prerequisites for an x86 build

`--arch x86` requires an **i686** toolchain (install it with MSYS2 if you do not
have one yet):

```bash
D:\msys64\usr\bin\pacman.exe -S mingw-w64-i686-gcc mingw-w64-i686-binutils
```

The build script prepends `D:\msys64\mingw32\bin` to `PATH` and sets
`MSYSTEM=MINGW32`; without that, gcc silently fails to spawn `cc1`/`as`/`ld`.

> ⚠️ **`windres` must match the target architecture.** The embedded-loader resource
> is compiled with the `windres.exe` sitting next to the gcc selected for that
> target. Feeding an x64 `windres` output to an x86 link makes the linker reject the
> `.res` file with **`file format not recognized`**.

### Toolchain overrides (environment variables)

| Variable | Default | Meaning |
|---|---|---|
| `WV2_GCC` / `WV2_GCC_X64` | `D:\winlibs\mingw64\bin\gcc.exe` | x64 GCC |
| `WV2_TCC` / `WV2_TCC_X64` | `D:\winlibs\tcc\tcc.exe` | TCC (x64 only) |
| `WV2_GCC_X86` | `D:\msys64\mingw32\bin\gcc.exe` | i686 GCC |
| `WV2_MSYS32` | `D:\msys64` | MSYS2 root (its `usr\bin` is added to `PATH`) |
| `WV2_TMP` | `D:\winlibs\tmp` | temp dir used by the compilers |
| `WV2_SDK` | `D:\WebView2-Python\sdk` | WebView2 SDK (needs `include\WebView2.h`) |

`windres` is not configured separately — it is always taken from the directory of
the compiler chosen for the target architecture.

---

## 🧪 Testing

```bash
python -m pytest pywv2/tests -v
python -m pywv2.tests.verify_pdf    # standalone PDF-printing verification
python -m pywv2.examples.basic      # example
```

---

## 📄 License

MIT © Dvs (DvsXT)
