diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7afa5e91..0d88a3080 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,39 @@ python build/build.py --config build/config/release.windows.yaml --chromium-src The build typically takes 1-3 hours on modern hardware (M4 Max, Ryzen 9, etc.). +### Building Without R2 Credentials (External Contributors) + +The browser build's `download_resources` step pulls pre-built `browseros_server` +binaries from a private Cloudflare R2 bucket. External contributors don't have +those credentials, and historically the build failed at validation with: + +``` +R2 configuration not set. Required env vars: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY +``` + +You can skip this step by setting `BROWSEROS_SKIP_R2_DOWNLOAD=1`: + +```bash +# macOS / Linux +export BROWSEROS_SKIP_R2_DOWNLOAD=1 +python build/build.py --config build/config/debug.macos.yaml --chromium-src /path/to/chromium/src --build + +# Windows (PowerShell) +$env:BROWSEROS_SKIP_R2_DOWNLOAD = "1" +python build/build.py --config build/config/debug.windows.yaml --chromium-src C:\path\to\chromium\src --build +``` + +**What this does:** the `download_resources` module short-circuits with a +warning and the build proceeds with whatever is already cached locally under +`resources/binaries/browseros_server/`. + +**Caveat:** subsequent steps that copy server binaries into the Chromium tree +(see `build/config/copy_resources.yaml`) will produce a partial build if that +cache is empty. This flag is intended for contributors working on **Chromium +patches, the build system, or the agent extension** — areas that don't depend +on the bundled server binary. If you need a working server binary too, please +reach out on Discord. + **For detailed instructions, see [Browser Build Guide](docs/BUILD.md).** ## Making Your First Contribution diff --git a/packages/browseros/build/common/env.py b/packages/browseros/build/common/env.py index e4317c1f3..7eb2d34a6 100644 --- a/packages/browseros/build/common/env.py +++ b/packages/browseros/build/common/env.py @@ -266,6 +266,18 @@ def has_r2_config(self) -> bool: self.r2_account_id and self.r2_access_key_id and self.r2_secret_access_key ) + @property + def skip_r2_download(self) -> bool: + """Whether to skip the R2 resource download step. + + External contributors without R2 credentials can set + BROWSEROS_SKIP_R2_DOWNLOAD=1 to bypass downloading the browseros_server + resource bundles. The build will proceed with whatever is already + cached locally under resources/binaries/browseros_server/. + """ + value = os.environ.get("BROWSEROS_SKIP_R2_DOWNLOAD", "") + return value.strip().lower() in ("1", "true", "yes", "on") + def has_sparkle_key(self) -> bool: """Check if Sparkle private key is available""" return bool(self.sparkle_private_key) diff --git a/packages/browseros/build/modules/storage/download.py b/packages/browseros/build/modules/storage/download.py index fb110d271..f3853ec49 100644 --- a/packages/browseros/build/modules/storage/download.py +++ b/packages/browseros/build/modules/storage/download.py @@ -15,6 +15,7 @@ from ...common.utils import ( log_info, log_success, + log_warning, get_platform, ) @@ -187,6 +188,12 @@ class DownloadResourcesModule(CommandModule): description = "Download resources from Cloudflare R2" def validate(self, context: Context) -> None: + if context.env.skip_r2_download: + # External contributors without R2 access opt in via + # BROWSEROS_SKIP_R2_DOWNLOAD=1. Don't gate on boto3 or R2 creds here; + # execute() will log and return early. + return + if not BOTO3_AVAILABLE: raise ValidationError( "boto3 library not installed - run: pip install boto3" @@ -195,7 +202,9 @@ def validate(self, context: Context) -> None: if not context.env.has_r2_config(): raise ValidationError( "R2 configuration not set. Required env vars: " - "R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY" + "R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY. " + "External contributors without R2 access can set " + "BROWSEROS_SKIP_R2_DOWNLOAD=1 to skip this step." ) config_path = context.get_download_resources_config() @@ -205,6 +214,16 @@ def validate(self, context: Context) -> None: ) def execute(self, context: Context) -> None: + if context.env.skip_r2_download: + log_warning( + "Skipping R2 resource download (BROWSEROS_SKIP_R2_DOWNLOAD is set).\n" + " Build will proceed with whatever is already cached under\n" + " resources/binaries/browseros_server/. Subsequent steps that\n" + " copy server binaries into the Chromium tree may fail or\n" + " produce a partial build if the cache is empty." + ) + return + log_info("\nDownloading resources from R2...") config_path = context.get_download_resources_config() diff --git a/packages/browseros/build/modules/storage/download_test.py b/packages/browseros/build/modules/storage/download_test.py index 6d598d540..524109dd7 100644 --- a/packages/browseros/build/modules/storage/download_test.py +++ b/packages/browseros/build/modules/storage/download_test.py @@ -9,9 +9,12 @@ import unittest import zipfile from pathlib import Path +from types import SimpleNamespace +from unittest import mock from build.modules.storage.download import ( ARTIFACT_METADATA_NAME, + DownloadResourcesModule, extract_artifact_zip, ) @@ -143,5 +146,51 @@ def _build_metadata(self, files: dict[str, bytes]) -> dict: } +class SkipR2DownloadTest(unittest.TestCase): + """Verify BROWSEROS_SKIP_R2_DOWNLOAD bypasses validation and execution. + + External contributors without R2 credentials need a way to run the build + pipeline without failing at the download step. + """ + + def _make_context(self, *, skip: bool) -> SimpleNamespace: + env = SimpleNamespace( + skip_r2_download=skip, + has_r2_config=lambda: False, + ) + return SimpleNamespace(env=env) + + def test_validate_skips_all_checks_when_flag_set(self) -> None: + module = DownloadResourcesModule() + context = self._make_context(skip=True) + + # Must not raise even though R2 config is missing. + module.validate(context) + + def test_validate_still_requires_r2_when_flag_unset(self) -> None: + from build.common.module import ValidationError + + module = DownloadResourcesModule() + context = self._make_context(skip=False) + + with self.assertRaises(ValidationError): + module.validate(context) + + def test_execute_returns_early_when_flag_set(self) -> None: + module = DownloadResourcesModule() + context = self._make_context(skip=True) + + # Mock log_warning to avoid console encoding issues on Windows hosts + # and to assert the user sees a clear notice. + with mock.patch( + "build.modules.storage.download.log_warning" + ) as log_warning, mock.patch( + "build.modules.storage.download.get_r2_client" + ) as get_client: + module.execute(context) + log_warning.assert_called_once() + get_client.assert_not_called() + + if __name__ == "__main__": unittest.main()