Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@ Turn any web app into a native-feeling Linux launcher. PWA Forge spins up isolat

## Installation

```bash
pip install pwa-forge
```

From source:
**Note:** PWA Forge is not yet published to PyPI. Install from source:

```bash
# Clone the repository
git clone https://github.com/bigr/pwa_forge.git
cd pwa_forge
pip install -e .

# Or install directly via pip with git
pip install git+https://github.com/bigr/pwa_forge.git
```

### Offline Installation

If you don't have internet access, you can run PWA Forge directly from the source directory:

```bash
# Extract the source code and add to PYTHONPATH
export PYTHONPATH="/path/to/pwa_forge/src:$PYTHONPATH"
python -m pwa_forge.cli --help
```

## Usage
Expand Down
9 changes: 8 additions & 1 deletion docs/Implementation-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,13 @@ pip install -e .
pip install git+https://github.com/bigr/pwa_forge.git
```

#### Offline Installation (No Internet)
```bash
# Extract source code and run directly via PYTHONPATH
export PYTHONPATH="/path/to/pwa_forge/src:$PYTHONPATH"
python -m pwa_forge.cli --help
```

#### From PyPI (Future)
```bash
pip install pwa-forge # Not yet available
Expand Down Expand Up @@ -881,7 +888,7 @@ pwa-forge config set default_browser chrome

## System Packaging Options

**pwa-forge** is currently distributed via PyPI (`pip install pwa-forge`). For broader distribution and easier system integration, consider packaging for native package managers.
**pwa-forge** is currently distributed via GitHub and can be installed from source. For broader distribution and easier system integration, future releases could be packaged for native package managers and PyPI.

### Recommended Packaging Formats

Expand Down
3 changes: 2 additions & 1 deletion docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ ERROR: pwa-forge requires Python>=3.10
```bash
python3.10 -m venv .venv
source .venv/bin/activate
pip install pwa-forge
pip install git+https://github.com/bigr/pwa_forge.git
# Or for offline: export PYTHONPATH="/path/to/pwa_forge/src:$PYTHONPATH"
```

### Missing Dependencies
Expand Down
8 changes: 8 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ pip install -e .
pip install git+https://github.com/bigr/pwa_forge.git
```

**Offline Installation:** If you don't have internet access, you can run PWA Forge directly:

```bash
# Extract source and add to PYTHONPATH
export PYTHONPATH="/path/to/pwa_forge/src:$PYTHONPATH"
python -m pwa_forge.cli --help
```

### Create Your First PWA

```bash
Expand Down
Binary file added docs/testing/report2.pdf
Binary file not shown.
84 changes: 58 additions & 26 deletions src/pwa_forge/commands/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import logging
import shutil
import subprocess
from pathlib import Path
from typing import Any
Expand All @@ -22,6 +23,11 @@ class HandlerCommandError(Exception):
def _find_browser_executable(browser: str, config: Config) -> Path:
"""Find the executable path for a browser.

Uses multiple detection strategies:
1. Configured path from config.browsers
2. Known platform-specific install paths
3. System PATH search (shutil.which) with multiple executable names

Args:
browser: Browser name (chrome, chromium, firefox, edge).
config: Configuration object.
Expand All @@ -32,36 +38,62 @@ def _find_browser_executable(browser: str, config: Config) -> Path:
Raises:
HandlerCommandError: If browser executable is not found.
"""
# Get browser path from config
# Strategy 1: Check configured path
browser_path_str = getattr(config.browsers, browser, None)
if not browser_path_str:
raise HandlerCommandError(f"Unknown browser: {browser}")

browser_path = Path(browser_path_str)
if browser_path_str:
browser_path = Path(browser_path_str)
if browser_path.exists():
logger.debug(f"Found {browser} via config: {browser_path}")
return browser_path

# Strategy 2: Check known install locations
known_paths = {
"chrome": [
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/snap/bin/chromium",
"/usr/local/bin/google-chrome",
],
"chromium": [
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
],
"firefox": [
"/usr/bin/firefox",
"/snap/bin/firefox",
"/usr/local/bin/firefox",
],
"edge": [
"/usr/bin/microsoft-edge",
"/opt/microsoft/msedge/microsoft-edge",
],
}

# Check if browser exists
if not browser_path.exists():
# Try to find it with 'which'
try:
result = subprocess.run(
["which", browser],
capture_output=True,
text=True,
check=True,
)
browser_path = Path(result.stdout.strip())
if browser_path.exists():
logger.info(f"Found {browser} at {browser_path}")
return browser_path
except subprocess.CalledProcessError:
pass
for path_str in known_paths.get(browser, []):
path = Path(path_str)
if path.exists():
logger.debug(f"Found {browser} at known location: {path}")
return path

# Strategy 3: Search system PATH with multiple executable names
executable_names = {
"chrome": ["google-chrome-stable", "google-chrome", "chrome"],
"chromium": ["chromium-browser", "chromium"],
"firefox": ["firefox"],
"edge": ["microsoft-edge", "edge"],
}

raise HandlerCommandError(
f"Browser executable not found: {browser_path_str}\n"
f" → Install {browser} or use a different browser with --browser"
)
for name in executable_names.get(browser, [browser]):
found_path = shutil.which(name)
if found_path:
logger.info(f"Found {browser} in PATH as '{name}': {found_path}")
return Path(found_path)

return browser_path
# Not found
raise HandlerCommandError(
f"Browser '{browser}' not found\n" f" → Install {browser} or use a different browser with --browser"
)


def generate_handler(
Expand Down
36 changes: 29 additions & 7 deletions tests/unit/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,47 @@ def test_find_browser_from_config(self, tmp_path: Path) -> None:
result = _find_browser_executable("firefox", config)
assert result == fake_browser

@patch("subprocess.run")
def test_find_browser_not_found(self, mock_run: MagicMock) -> None:
@patch("shutil.which")
@patch("pathlib.Path.exists")
def test_find_browser_not_found(self, mock_exists: MagicMock, mock_which: MagicMock) -> None:
"""Test error when browser not found."""
config = Config()
config.browsers.firefox = "/nonexistent/firefox"

# Mock 'which' command to fail
mock_run.side_effect = subprocess.CalledProcessError(1, "which")
# Mock all paths as non-existent and which returns None
mock_exists.return_value = False
mock_which.return_value = None

with pytest.raises(HandlerCommandError, match="Browser executable not found"):
with pytest.raises(HandlerCommandError, match="Browser 'firefox' not found"):
_find_browser_executable("firefox", config)

def test_find_browser_unknown(self) -> None:
@patch("shutil.which")
@patch("pathlib.Path.exists")
def test_find_browser_unknown(self, mock_exists: MagicMock, mock_which: MagicMock) -> None:
"""Test error for unknown browser."""
config = Config()

with pytest.raises(HandlerCommandError, match="Unknown browser"):
# Mock all paths as non-existent and which returns None
mock_exists.return_value = False
mock_which.return_value = None

with pytest.raises(HandlerCommandError, match="Browser 'unknown' not found"):
_find_browser_executable("unknown", config)

@patch("shutil.which")
@patch("pathlib.Path.exists")
def test_find_browser_via_which(self, mock_exists: MagicMock, mock_which: MagicMock) -> None:
"""Test finding browser via shutil.which() when not in standard locations."""
config = Config()
config.browsers.firefox = "/nonexistent/firefox"

# Mock all paths as non-existent, but which finds it
mock_exists.return_value = False
mock_which.return_value = "/snap/bin/firefox"

result = _find_browser_executable("firefox", config)
assert result == Path("/snap/bin/firefox")


class TestGenerateHandler:
"""Test generate_handler function."""
Expand Down
Loading