Compare commits

...
1 Commits
9 changed files with 97 additions and 18 deletions

No files matched your search

+6
View File
@@ -38,6 +38,12 @@ jobs:
pip install -r requirements.txt
pip install pyinstaller
- name: Download Tor Binaries
shell: bash
run: |
chmod +x scripts/download_tor.sh
./scripts/download_tor.sh
- name: Build with PyInstaller
shell: bash
run: |
+3 -2
View File
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
[project]
name = "voxvera"
version = "0.1.5"
version = "0.1.6"
description = "VoxVera CLI utilities"
readme = "README.md"
authors = [{name = "VoxVera"}]
@@ -26,9 +26,10 @@ dependencies = [
"Pillow",
"jsmin",
"htmlmin2",
"pypdf",
"wcwidth",
"onionshare-cli",
]
[project.optional-dependencies]
test = [
"pytest",
+2
View File
@@ -6,3 +6,5 @@ jsmin
htmlmin2
pypdf
wcwidth
onionshare-cli
+3
View File
@@ -19,6 +19,9 @@ mkdir -p voxvera/resources/bin
echo "Building VoxVera binary for $(uname)..."
echo "Ensuring Tor binaries are downloaded..."
bash scripts/download_tor.sh
# Use python module to call pyinstaller
PY_CMD="python3 -m PyInstaller"
+8 -3
View File
@@ -112,10 +112,15 @@ def test_check_all_present(capsys, monkeypatch):
def test_check_missing(capsys, monkeypatch):
def fake_which(cmd):
return None if cmd == "onionshare-cli" else "/usr/bin/" + cmd
import builtins
original_import = builtins.__import__
monkeypatch.setattr(shutil, "which", fake_which)
def fake_import(name, *args, **kwargs):
if name == "onionshare_cli":
raise ImportError("Mocked missing")
return original_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
cli.main(["check"])
captured = capsys.readouterr()
assert "onionshare-cli" in captured.out
+9 -3
View File
@@ -46,7 +46,11 @@ def check_tor_running():
def check_onionshare_cli():
"""Check if onionshare-cli is installed."""
return shutil.which("onionshare-cli") is not None
try:
import onionshare_cli # noqa: F401
return True
except ImportError:
return False
@pytest.fixture(scope="module")
@@ -222,12 +226,14 @@ class TestFullWorkflow:
host_dir = tmp_path / "host" / "reachtest"
assert host_dir.is_dir(), f"Build did not create {host_dir}"
# Launch onionshare-cli directly against the built host directory
# Launch onionshare-cli directly against the built host directory using our internal wrapper
logfile = host_dir / "onionshare.log"
log_fh = open(logfile, "w")
cmd = [
"onionshare-cli",
sys.executable,
"-m", "voxvera.cli",
"_internal_onionshare",
"--website",
"--public",
"--persistent",
+6 -3
View File
@@ -115,9 +115,12 @@ def test_portable_bundle_integrity(tmp_path, monkeypatch):
shutil.copytree(REPO_ROOT / "voxvera" / "locales", voxvera_dir / "locales")
os.makedirs(voxvera_dir / "vendor", exist_ok=True)
(voxvera_dir / "vendor" / "anchor.txt").write_text("anchor")
(project_root / "voxvera-run.sh").write_text("echo run")
(project_root / "uninstall.sh").write_text("echo uninstall")
(project_root / "uninstall.ps1").write_text("echo uninstall")
# Copy real scripts from repo
for script in ["voxvera-run.sh", "uninstall.sh", "uninstall.ps1", "install.sh", "install.ps1", "setup.sh"]:
if (REPO_ROOT / script).exists():
shutil.copy(REPO_ROOT / script, project_root / script)
# ROOT in cli.py points to the voxvera/ directory
monkeypatch.setattr(cli, "ROOT", voxvera_dir)
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
__version__ = "0.1.5"
__version__ = "0.1.6"
# Add the vendor directory to sys.path as a fallback.
# For standard installs (pip/pipx), the installed dependencies in the venv
+59 -6
View File
@@ -64,6 +64,14 @@ def t(path: str, **kwargs) -> str:
def require_cmd(cmd: str):
if cmd == "onionshare-cli":
try:
import onionshare_cli # noqa: F401
return True
except ImportError:
print("Required tool 'onionshare-cli' not bundled or installed.", file=sys.stderr)
return False
if shutil.which(cmd) is None:
print(
f"Required command '{cmd}' not found. Please install it.", file=sys.stderr
@@ -75,10 +83,11 @@ def require_cmd(cmd: str):
def check_deps():
console = Console()
# External CLI tools still required at runtime
cli_tools = ["onionshare-cli"]
# External CLI tools still required at runtime (none anymore if bundled)
cli_tools = []
# Python packages used by the build pipeline
py_packages = {
"onionshare-cli": "onionshare_cli",
"qrcode": "qrcode",
"PIL (Pillow)": "PIL",
"jsmin": "jsmin",
@@ -445,6 +454,11 @@ def build_assets(
"landing": l_data.get("landing", {})
}
# Override landing data with user custom config data so JS doesn't overwrite it
for field in ["title", "subtitle", "headline", "content", "url_message"]:
if field in data and data[field]:
all_locales[code]["landing"][field] = data[field]
html = html.replace("{{locales}}", json.dumps(all_locales))
# 1. Statically replace localization tokens {{t_web_...}}
@@ -554,10 +568,29 @@ def get_tor_ports():
return socks_port, ctl_port
def serve(config_path: str) -> str | None:
if not require_cmd("onionshare-cli"):
def _internal_onionshare():
"""Hidden command to run the bundled onionshare-cli."""
import platform
# Try to add bundled Tor to PATH so OnionShare can find it
system = platform.system().lower()
platform_dir = "win" if "windows" in system else "mac" if "darwin" in system else "linux"
tor_dir = ROOT / "resources" / "tor" / platform_dir
if tor_dir.exists():
os.environ["PATH"] = str(tor_dir) + os.pathsep + os.environ.get("PATH", "")
try:
from onionshare_cli import main
# sys.argv is ['voxvera', '_internal_onionshare', '--website', ...]
# We need to make it look like ['onionshare-cli', '--website', ...]
sys.argv = ["onionshare-cli"] + sys.argv[2:]
sys.exit(main())
except ImportError:
print("Error: onionshare-cli not bundled or installed.", file=sys.stderr)
sys.exit(1)
def serve(config_path: str) -> str | None:
socks, ctl = get_tor_ports()
folder_name = load_config(config_path)["folder_name"]
@@ -572,8 +605,11 @@ def serve(config_path: str) -> str | None:
env["TOR_SOCKS_PORT"] = socks
env["TOR_CONTROL_PORT"] = ctl
# Run the bundled onionshare-cli via our hidden command
# sys.argv[0] is the current executable (python script or PyInstaller binary)
cmd = [
"onionshare-cli",
sys.argv[0],
"_internal_onionshare",
"--website",
"--public",
"--persistent",
@@ -584,6 +620,12 @@ def serve(config_path: str) -> str | None:
"-v",
str(dir_path),
]
# If running via 'python -m voxvera.cli', sys.argv[0] might be the script path,
# so we should ensure it's executed correctly if not a PyInstaller binary.
if not getattr(sys, 'frozen', False) and sys.argv[0].endswith('.py'):
cmd = [sys.executable, sys.argv[0]] + cmd[1:]
log_fh = open(logfile, "w")
proc = subprocess.Popen(cmd, stdout=log_fh, stderr=subprocess.STDOUT, env=env)
@@ -848,6 +890,11 @@ def build_site():
"landing": l_data.get("landing", {})
}
# Override landing data with user custom config data so JS doesn't overwrite it
for field in ["title", "subtitle", "headline", "content", "url_message"]:
if field in data and data[field]:
all_locales[code]["landing"][field] = data[field]
html = html.replace("{{locales}}", json.dumps(all_locales))
# 1. Statically replace localization tokens (web tokens)
@@ -1228,8 +1275,14 @@ def main(argv=None):
sub.add_parser("build-docs", help="Generate localized documentation from templates")
sub.add_parser("build-site", help="Refresh the main site/ directory assets and bundle")
sub.add_parser("manage", help="Manage VoxVera servers interactively")
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
args, unknown_args = parser.parse_known_args(argv)
if args.command == "_internal_onionshare":
_internal_onionshare()
return
args = parser.parse_args(argv)
config_path = Path(args.config).resolve()
# Determine language