BoltClam 1.0.0: ClamAV desktop protection with Bolt sign-in

Adds the PyQt6 Windows client (Homepage, Scan & results, About & updates, Settings), ClamAV scanning and quarantine, browser-download watch with an exclusive scan gate, Bolt loopback/PKCE sign-in with DPAPI key storage, Gitea release feed with checksum verification, opt-in diagnostics, and Windows/portable build tooling.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
TheMajesticMagician
2026-10-25 16:21:14 -06:00
co-authored by Claude Code
commit addd00364d
76 changed files with 12503 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Build the portable Windows BoltClam package from a Linux/macOS host.
The result is a folder and zip containing:
BoltClam.exe tiny GUI launcher (packaging/launcher.c)
pythonw.exe official Windows embeddable Python 3.11
python311._pth points at app/ and the Windows PyQt6 wheels
app/boltclam/ BoltClam source
resources/ fonts and icon assets
Lib/site-packages/ Windows PyQt6 wheels
This is the no-Windows-machine path. On a Windows build host, prefer
`build_windows.ps1` / PyInstaller for a branded one-file EXE and Inno Setup
installer. This script downloads public build inputs and never uses a
platform-specific Python interpreter to run the Windows app.
"""
from __future__ import annotations
import argparse
import hashlib
import os
import shutil
import subprocess
import sys
import tarfile
import urllib.request
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / "build" / "portable"
CACHE = ROOT / "build" / "cache"
DIST = ROOT / "dist"
VERSION = "1.0.0"
PYTHON_VERSION = "3.11.9"
PYTHON_URL = (f"https://www.python.org/ftp/python/{PYTHON_VERSION}/"
f"python-{PYTHON_VERSION}-embed-amd64.zip")
PYTHON_SHA256 = "009d6bf7e3b2ddca3d784fa09f90fe54336d5b60f0e0f305c37f400bf83cfd3b"
PYQT_PACKAGES = ("PyQt6==6.11.0", "PyQt6-Qt6==6.11.2", "PyQt6-sip==13.13.0")
ZIG_VERSION = "0.14.1"
ZIG_URL = f"https://ziglang.org/download/{ZIG_VERSION}/zig-x86_64-linux-{ZIG_VERSION}.tar.xz"
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download(url: str, destination: Path, expected: str = "") -> Path:
destination.parent.mkdir(parents=True, exist_ok=True)
if destination.is_file() and (not expected or _sha256(destination) == expected):
return destination
print(f"Downloading {url}")
temp = destination.with_suffix(destination.suffix + ".part")
with urllib.request.urlopen(url, timeout=120) as response, open(temp, "wb") as handle:
shutil.copyfileobj(response, handle)
if expected and _sha256(temp) != expected:
temp.unlink(missing_ok=True)
raise RuntimeError(f"Checksum mismatch for {url}")
os.replace(temp, destination)
return destination
def find_zig(explicit: str) -> Path | None:
if explicit:
return Path(explicit)
found = shutil.which("zig")
if found:
return Path(found)
return None
def download_zig() -> Path:
archive = download(ZIG_URL, CACHE / f"zig-{ZIG_VERSION}.tar.xz")
target = CACHE / f"zig-x86_64-linux-{ZIG_VERSION}"
if not (target / "zig").is_file():
print("Extracting Zig")
with tarfile.open(archive, "r:xz") as tar:
tar.extractall(CACHE)
return target / "zig"
def build(use_zig: str = "", keep_build: bool = True) -> Path:
if BUILD.exists():
shutil.rmtree(BUILD)
BUILD.mkdir(parents=True)
CACHE.mkdir(parents=True, exist_ok=True)
python_zip = download(PYTHON_URL, CACHE / f"python-{PYTHON_VERSION}-embed-amd64.zip",
PYTHON_SHA256)
with zipfile.ZipFile(python_zip) as archive:
archive.extractall(BUILD)
app = BUILD / "app"
app.mkdir()
shutil.copytree(ROOT / "src" / "boltclam", app / "boltclam",
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"))
# paths.resource_dir() resolves to parents[2]/resources, i.e. the
# portable root, for app/boltclam/paths.py. Keep resources there.
shutil.copytree(ROOT / "resources", BUILD / "resources")
site_packages = BUILD / "Lib" / "site-packages"
site_packages.mkdir(parents=True)
print("Installing Windows PyQt6 wheels")
subprocess.check_call([
sys.executable, "-m", "pip", "install", "--quiet", "--target", str(site_packages),
"--platform", "win_amd64", "--python-version", "3.11",
"--implementation", "cp", "--abi", "cp311", "--only-binary=:all:",
*PYQT_PACKAGES,
])
(BUILD / "python311._pth").write_text(
"python311.zip\n.\nLib\\site-packages\napp\nimport site\n", encoding="utf-8")
zig = find_zig(use_zig)
launcher_source = ROOT / "packaging" / "launcher.c"
launcher_binary = ROOT / "packaging" / "BoltClam-launcher.exe"
if zig is None and launcher_binary.is_file():
print("Using the checked-in portable launcher")
shutil.copy2(launcher_binary, BUILD / "BoltClam.exe")
else:
if zig is None:
zig = download_zig()
print(f"Compiling launcher with {zig}")
subprocess.check_call([
str(zig), "cc", "-target", "x86_64-windows-gnu", "-O2",
"-Wl,--subsystem,windows", str(launcher_source),
"-o", str(BUILD / "BoltClam.exe"),
])
(BUILD / "README.txt").write_text(
"BoltClam portable for Windows x64\r\n"
"=================================\r\n\r\n"
"Double-click BoltClam.exe. Sign in with your Bolt account when the\r\n"
"browser opens. ClamAV is installed separately: install it from\r\n"
"https://www.clamav.net/downloads, then open Settings and point BoltClam\r\n"
"at clamscan.exe (Auto-detect usually finds it).\r\n\r\n"
"The first definition update downloads ClamAV signatures into\r\n"
"%LOCALAPPDATA%\\BoltClam\\clamav-db. No administrator rights are needed\r\n"
"for BoltClam's own data.\r\n\r\n"
"This build is unsigned. Windows SmartScreen may warn about a new\r\n"
"publisher; use the checksum published with the release.\r\n\r\n"
"Keep the whole folder together. BoltClam.exe is only a launcher for\r\n"
"pythonw.exe and app/.\r\n\r\n"
"Troubleshooting: if BoltClam.exe does nothing, open a command prompt in\r\n"
"this folder and run `python.exe -m boltclam` to see the startup error.\r\n",
encoding="utf-8")
DIST.mkdir(parents=True, exist_ok=True)
zip_path = DIST / f"BoltClam-{VERSION}-windows-x64-portable.zip"
print(f"Writing {zip_path}")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
for path in sorted(BUILD.rglob("*")):
if path.is_file():
archive.write(path, arcname="BoltClam/" + path.relative_to(BUILD).as_posix())
digest = _sha256(zip_path)
(DIST / "SHA256SUMS.txt").write_text(f"{digest} {zip_path.name}\n", encoding="utf-8")
print(f"{zip_path} ({zip_path.stat().st_size / (1024 * 1024):.1f} MiB)")
print(f"SHA-256 {digest}")
return zip_path
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--zig", default="", help="path to a Zig executable")
args = parser.parse_args()
build(use_zig=args.zig)
return 0
if __name__ == "__main__":
raise SystemExit(main())