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 0000000..1241782 Binary files /dev/null and b/docs/testing/report2.pdf differ 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."""