From dcda9430c7137457c72151ff8223324b7f122097 Mon Sep 17 00:00:00 2001 From: Pavel Klinger Date: Fri, 24 Oct 2025 10:24:08 +0200 Subject: [PATCH] :sparkles: Improve handler browser detection and add offline installation docs - Refactor _find_browser_executable() in handler.py to use same robust strategy as add.py - Add platform-specific path checking and shutil.which() fallback - Support Snap, Flatpak, and custom browser installations - Add comprehensive offline installation instructions across all docs - Update README.md, USAGE.md, TROUBLESHOOTING.md with PYTHONPATH method - Fix test expectations to match new error messages - Add test for browser detection via PATH fallback - Resolves second-round testing issue #9 (handler generation failures) - Resolves second-round testing issue #6 (offline installation unclear) --- README.md | 20 +++++-- docs/Implementation-specification.md | 9 ++- docs/TROUBLESHOOTING.md | 3 +- docs/USAGE.md | 8 +++ docs/testing/report2.pdf | Bin 0 -> 45285 bytes src/pwa_forge/commands/handler.py | 84 ++++++++++++++++++--------- tests/unit/test_handler.py | 36 +++++++++--- 7 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 docs/testing/report2.pdf diff --git a/README.md b/README.md index 5774d8e..ffef542 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/Implementation-specification.md b/docs/Implementation-specification.md index a7bae6d..fad712d 100644 --- a/docs/Implementation-specification.md +++ b/docs/Implementation-specification.md @@ -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 @@ -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 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 61c50f8..e7168a6 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -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 diff --git a/docs/USAGE.md b/docs/USAGE.md index 1d80881..5fcedb9 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -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 diff --git a/docs/testing/report2.pdf b/docs/testing/report2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1241782a8d1fb7f716c02730e32d986d49da1b40 GIT binary patch literal 45285 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

wlP{;hEtsH$hS?zNdc`nisljQ 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 literal 0 HcmV?d00001 diff --git a/src/pwa_forge/commands/handler.py b/src/pwa_forge/commands/handler.py index 02b92b8..d41d8d9 100644 --- a/src/pwa_forge/commands/handler.py +++ b/src/pwa_forge/commands/handler.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import shutil import subprocess from pathlib import Path from typing import Any @@ -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. @@ -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( diff --git a/tests/unit/test_handler.py b/tests/unit/test_handler.py index f2df58b..7e5d047 100644 --- a/tests/unit/test_handler.py +++ b/tests/unit/test_handler.py @@ -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."""