Add PyInstaller release workflow

This commit is contained in:
Golumpa 2026-06-17 14:54:25 +01:00
parent fefe3a1567
commit c5373aa9e0
9 changed files with 320 additions and 3 deletions

166
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,166 @@
name: Release
on:
workflow_dispatch:
push:
branches: [main]
permissions:
contents: write
jobs:
build-windows:
name: Build Windows
if: github.ref == 'refs/heads/main'
runs-on: windows-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Get Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[build]"
- name: Test
run: python -m unittest discover -s tests
- name: Build
run: python -m PyInstaller --clean --noconfirm "packaging/NTE History Exporter.spec"
- name: Smoke Test
run: .\dist\nte-history-exporter.exe --help
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: windows
path: dist/nte-history-exporter.exe
build-linux:
name: Build Linux
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Install libpcap
run: sudo apt-get update && sudo apt-get install -y libpcap-dev
- name: Get Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[build]"
- name: Test
run: python -m unittest discover -s tests
- name: Build
run: python -m PyInstaller --clean --noconfirm "packaging/NTE History Exporter.spec"
- name: Smoke Test
run: ./dist/nte-history-exporter --help
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: linux
path: dist/nte-history-exporter
build-macos:
name: Build Macos
if: github.ref == 'refs/heads/main'
runs-on: macos-15-intel
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Install libpcap
run: brew update && brew install libpcap
- name: Get Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[build]"
- name: Test
run: python -m unittest discover -s tests
- name: Build
run: python -m PyInstaller --clean --noconfirm "packaging/NTE History Exporter.spec"
- name: Smoke Test
run: ./dist/nte-history-exporter --help
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: macos
path: dist/nte-history-exporter
release:
name: Release
if: github.ref == 'refs/heads/main'
needs: [build-windows, build-linux, build-macos]
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Get package version
id: package-version
run: |
version="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Download Artifacts
uses: actions/download-artifact@v4
- name: Rename
run: |
version="${{ steps.package-version.outputs.version }}"
cp windows/nte-history-exporter.exe nte-history-exporter.exe
zip nte-history-exporter-v${version}-x86_64-pc-windows-msvc.zip nte-history-exporter.exe
cp linux/nte-history-exporter nte-history-exporter
chmod +x nte-history-exporter
cp nte-history-exporter nte-history-exporter-linux
zip nte-history-exporter-v${version}-x86_64-unknown-linux-gnu.zip nte-history-exporter
cp macos/nte-history-exporter nte-history-exporter
chmod +x nte-history-exporter
cp nte-history-exporter nte-history-exporter-macos
zip nte-history-exporter-v${version}-x86_64-apple-darwin.zip nte-history-exporter
- name: Release
uses: marvinpinto/action-automatic-releases@latest
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
automatic_release_tag: v${{ steps.package-version.outputs.version }}
prerelease: false
title: v${{ steps.package-version.outputs.version }} Release
files: |
nte-history-exporter.exe
nte-history-exporter-linux
nte-history-exporter-macos
nte-history-exporter-v${{ steps.package-version.outputs.version }}-x86_64-pc-windows-msvc.zip
nte-history-exporter-v${{ steps.package-version.outputs.version }}-x86_64-unknown-linux-gnu.zip
nte-history-exporter-v${{ steps.package-version.outputs.version }}-x86_64-apple-darwin.zip

45
docs/packaging.md Normal file
View file

@ -0,0 +1,45 @@
# Packaging
## Windows local build
Build the Windows console executable with:
```powershell
.\tools\build-windows.ps1
```
The output is:
```text
dist\nte-history-exporter.exe
```
Run live capture from an elevated PowerShell prompt:
```powershell
.\dist\nte-history-exporter.exe --live
```
## Packet capture runtime
Windows live capture defaults to `--capture-backend auto`, which tries an installed system Npcap runtime first, then the built-in raw-socket backend. If `--capture-backend libpcap` is specified, raw fallback is disabled. If `--capture-backend raw` is specified, only the Windows raw-socket backend is used.
Npcap is Windows-only; Linux and macOS use the system libpcap runtime.
The GitHub Actions release does not download or bundle Npcap. The Windows executable can use a user's installed Npcap runtime when present, then falls back to raw sockets. A future pktmon backend can provide the same kind of no-extra-install Windows path that stardb-exporter uses.
Npcap is not committed or bundled by default. Windows users who want the Npcap backend should install Npcap separately. Users who do not install Npcap can still use the Windows raw-socket fallback.
## Cross-platform CI shape
Use one GitHub Actions job per OS. PyInstaller must build on the target platform, so Windows produces `.exe`, macOS produces a macOS binary, and Linux produces a Linux binary.
This repository has a manual release workflow at:
```text
.github\workflows\release.yml
```
It builds Windows, Linux, and macOS artifacts, then publishes a `v<version>` GitHub release with raw binaries and versioned zip files.
Linux runners install the libpcap runtime for parity with live-capture usage, but the build itself uses `ctypes` and does not link against libpcap at package time.

View file

@ -0,0 +1,47 @@
# -*- mode: python ; coding: utf-8 -*-
from pathlib import Path
block_cipher = None
root = Path(SPECPATH).parent
a = Analysis(
[str(root / "src" / "nte_history_exporter" / "cli.py")],
pathex=[str(root / "src")],
binaries=[],
datas=[(str(root / "mappings"), "mappings")],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name="nte-history-exporter",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -5,6 +5,9 @@ description = "Cross-platform Neverness to Everness pull-history exporter."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [] dependencies = []
[project.optional-dependencies]
build = ["pyinstaller>=6.0"]
[project.scripts] [project.scripts]
nte-history-exporter = "nte_history_exporter.cli:main" nte-history-exporter = "nte_history_exporter.cli:main"

View file

@ -24,7 +24,10 @@ def build_parser() -> argparse.ArgumentParser:
"--capture-backend", "--capture-backend",
choices=["auto", "libpcap", "raw"], choices=["auto", "libpcap", "raw"],
default="auto", default="auto",
help="live capture backend; auto prefers Npcap/libpcap and falls back to raw sockets on Windows", help=(
"live capture backend; auto prefers Npcap/libpcap and falls back to raw sockets on Windows "
"(default: %(default)s)"
),
) )
parser.add_argument("--copy-clipboard", action="store_true", help="copy a single live export to clipboard") parser.add_argument("--copy-clipboard", action="store_true", help="copy a single live export to clipboard")
parser.add_argument("--debug", action="store_true", help="also write research CSVs next to the JSON exports") parser.add_argument("--debug", action="store_true", help="also write research CSVs next to the JSON exports")

View file

@ -103,7 +103,8 @@ def _load_library() -> ctypes.CDLL:
npcap_dir = _windows_npcap_directory() npcap_dir = _windows_npcap_directory()
if npcap_dir.is_dir() and hasattr(os, "add_dll_directory"): if npcap_dir.is_dir() and hasattr(os, "add_dll_directory"):
dll_directory = os.add_dll_directory(str(npcap_dir)) dll_directory = os.add_dll_directory(str(npcap_dir))
candidates.extend([str(npcap_dir / "wpcap.dll"), "wpcap.dll"]) candidates.append(str(npcap_dir / "wpcap.dll"))
candidates.append("wpcap.dll")
elif sys.platform == "darwin": elif sys.platform == "darwin":
candidates.extend( candidates.extend(
[ [

View file

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import json import json
import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[2] PROJECT_ROOT = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parents[2]))
MAPPINGS_DIR = PROJECT_ROOT / "mappings" MAPPINGS_DIR = PROJECT_ROOT / "mappings"

View file

@ -38,6 +38,7 @@ from nte_history_exporter.live_capture.libpcap import (
DLT_LOOP, DLT_LOOP,
DLT_RAW, DLT_RAW,
_extract_ipv4_frame, _extract_ipv4_frame,
_load_library,
LibpcapUnavailable, LibpcapUnavailable,
) )
from nte_history_exporter.live_capture.windows_raw import parse_ipv4_packet from nte_history_exporter.live_capture.windows_raw import parse_ipv4_packet
@ -138,6 +139,29 @@ class BoundaryExportTests(unittest.TestCase):
raw_capture.assert_not_called() raw_capture.assert_not_called()
def test_windows_libpcap_load_prefers_system_then_path(self):
system_dir = Path("C:/Windows/System32/Npcap")
loaded = Mock()
with (
patch("nte_history_exporter.live_capture.libpcap.sys.platform", "win32"),
patch("nte_history_exporter.live_capture.libpcap._windows_npcap_directory", return_value=system_dir),
patch("nte_history_exporter.live_capture.libpcap.Path.is_dir", return_value=True),
patch("nte_history_exporter.live_capture.libpcap.os.add_dll_directory", return_value=Mock()),
patch(
"nte_history_exporter.live_capture.libpcap.ctypes.CDLL",
side_effect=[OSError("system missing"), loaded],
) as cdll,
):
self.assertIs(_load_library(), loaded)
self.assertEqual(
[call.args[0] for call in cdll.call_args_list],
[
str(system_dir / "wpcap.dll"),
"wpcap.dll",
],
)
def test_libpcap_link_layers_extract_ipv4_packets(self): def test_libpcap_link_layers_extract_ipv4_packets(self):
ip_packet = bytes.fromhex("4500001c0000000040110000c0000201c6336402") + bytes(8) ip_packet = bytes.fromhex("4500001c0000000040110000c0000201c6336402") + bytes(8)
ethernet = bytes(12) + bytes.fromhex("0800") + ip_packet ethernet = bytes(12) + bytes.fromhex("0800") + ip_packet

27
tools/build-windows.ps1 Normal file
View file

@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
Set-Location (Split-Path -Parent $PSScriptRoot)
$python = Join-Path $PWD '.venv\Scripts\python.exe'
if (-not (Test-Path $python)) {
python -m venv .venv
}
& $python -m pip install --upgrade pip
& $python -m pip install -e '.[build]'
& $python -m PyInstaller --clean --noconfirm 'packaging\NTE History Exporter.spec'
if ($LASTEXITCODE -ne 0) {
throw "PyInstaller failed with exit code $LASTEXITCODE"
}
$exe = Join-Path $PWD 'dist\nte-history-exporter.exe'
if (-not (Test-Path $exe)) {
throw "Expected build output was not created: $exe"
}
Write-Host "Built $exe"
& $exe --help
if ($LASTEXITCODE -ne 0) {
throw "Built executable smoke test failed with exit code $LASTEXITCODE"
}