diff --git a/.gitignore b/.gitignore index c865747..7276156 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ **/logs **/old-scripts **/__pycache__/** +nxtscape-cli-access.json diff --git a/build/build.py b/build/build.py index 5d2d2ef..a026326 100755 --- a/build/build.py +++ b/build/build.py @@ -23,6 +23,7 @@ from modules.string_replaces import apply_string_replacements from modules.inject import inject_version from modules.configure import configure from modules.compile import build +from modules.gcs import upload_package_artifacts, upload_signed_artifacts # Platform-specific imports if IS_MACOS: @@ -76,6 +77,7 @@ def build_main( chromium_src_dir: Optional[Path] = None, slack_notifications: bool = False, patch_interactive: bool = False, + upload_gcs: bool = True, # Default to uploading to GCS ): """Main build orchestration""" log_info("šŸš€ Nxtscape Build System") @@ -270,6 +272,10 @@ def build_main( package(ctx) if slack_notifications: notify_build_step(f"[{ctx.architecture}] Completed DMG creation") + + # Upload to GCS after packaging + if upload_gcs: + upload_package_artifacts(ctx) built_contexts.append(ctx) @@ -327,6 +333,15 @@ def build_main( package_universal(built_contexts) if slack_notifications: notify_build_step("[Universal] Completed DMG package creation") + + # Upload universal package to GCS + if upload_gcs: + # Use the first context with universal architecture override + universal_ctx = built_contexts[0] + original_arch = universal_ctx.architecture + universal_ctx.architecture = "universal" + upload_package_artifacts(universal_ctx) + universal_ctx.architecture = original_arch # Summary elapsed = time.time() - start_time @@ -426,6 +441,12 @@ def build_main( default=False, help="Ask for confirmation before applying each patch", ) +@click.option( + "--no-gcs-upload", + is_flag=True, + default=False, + help="Skip uploading artifacts to Google Cloud Storage", +) def main( config, clean, @@ -442,6 +463,7 @@ def main( add_replace, string_replace, patch_interactive, + no_gcs_upload, ): """Simple build system for Nxtscape Browser""" @@ -532,6 +554,7 @@ def main( chromium_src_dir=chromium_src, slack_notifications=slack_notifications, patch_interactive=patch_interactive, + upload_gcs=not no_gcs_upload, # Invert the flag ) diff --git a/build/modules/gcs.py b/build/modules/gcs.py new file mode 100644 index 0000000..c95f84e --- /dev/null +++ b/build/modules/gcs.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Google Cloud Storage upload module for Nxtscape build artifacts +""" + +import os +from pathlib import Path +from typing import List, Optional +from context import BuildContext +from utils import log_info, log_error, log_success, log_warning, IS_WINDOWS, IS_MACOS, join_paths + +# Try to import google-cloud-storage +try: + from google.cloud import storage + from google.oauth2 import service_account + GCS_AVAILABLE = True +except ImportError: + GCS_AVAILABLE = False + +# Service account file name +SERVICE_ACCOUNT_FILE = "nxtscape-cli-access.json" + + +def upload_to_gcs(ctx: BuildContext, file_paths: List[Path]) -> bool: + """Upload build artifacts to Google Cloud Storage""" + if not GCS_AVAILABLE: + log_warning("google-cloud-storage not installed. Skipping GCS upload.") + log_info("Install with: pip install google-cloud-storage") + return True # Not a fatal error + + if not file_paths: + log_info("No files to upload to GCS") + return True + + # Determine platform subdirectory + if IS_WINDOWS: + platform_dir = "win" + elif IS_MACOS: + platform_dir = "macos" + else: + platform_dir = "linux" + + # Build GCS path: gs://nxtscape/resources/// + bucket_name = "nxtscape" + gcs_prefix = f"resources/{ctx.nxtscape_version}/{platform_dir}" + + log_info(f"\nā˜ļø Uploading artifacts to gs://{bucket_name}/{gcs_prefix}/") + + # Check for service account file + service_account_path = join_paths(ctx.root_dir, SERVICE_ACCOUNT_FILE) + if not service_account_path.exists(): + log_error(f"Service account file not found: {SERVICE_ACCOUNT_FILE}") + log_info(f"Please place the service account JSON file at: {service_account_path}") + return False + + try: + # Initialize GCS client with service account + credentials = service_account.Credentials.from_service_account_file( + str(service_account_path) + ) + client = storage.Client(credentials=credentials) + bucket = client.bucket(bucket_name) + + uploaded_files = [] + + for file_path in file_paths: + if not file_path.exists(): + log_warning(f"File not found, skipping: {file_path}") + continue + + # Determine blob name (file name in GCS) + blob_name = f"{gcs_prefix}/{file_path.name}" + + try: + blob = bucket.blob(blob_name) + + log_info(f"šŸ“¤ Uploading {file_path.name}...") + blob.upload_from_filename(str(file_path)) + + # Make the blob publicly readable + blob.make_public() + + public_url = f"https://storage.googleapis.com/{bucket_name}/{blob_name}" + uploaded_files.append(public_url) + log_success(f"āœ“ Uploaded: {public_url}") + + except Exception as e: + log_error(f"Failed to upload {file_path.name}: {e}") + return False + + if uploaded_files: + log_success(f"\nā˜ļø Successfully uploaded {len(uploaded_files)} file(s) to GCS") + log_info("\nPublic URLs:") + for url in uploaded_files: + log_info(f" {url}") + + return True + + except Exception as e: + log_error(f"GCS upload failed: {e}") + return False + + +def upload_package_artifacts(ctx: BuildContext) -> bool: + """Upload package artifacts (DMG, ZIP, EXE) to GCS""" + log_info("\nā˜ļø Preparing to upload package artifacts to GCS...") + + artifacts = [] + + if IS_MACOS: + # Look for DMG files + dmg_dir = ctx.root_dir / "dmg" + if dmg_dir.exists(): + artifacts.extend(dmg_dir.glob("*.dmg")) + + elif IS_WINDOWS: + # Look for installer and ZIP files + dist_dir = ctx.root_dir / "dist" + if dist_dir.exists(): + artifacts.extend(dist_dir.glob("*.exe")) + artifacts.extend(dist_dir.glob("*.zip")) + + if not artifacts: + log_info("No package artifacts found to upload") + return True + + log_info(f"Found {len(artifacts)} artifact(s) to upload:") + for artifact in artifacts: + log_info(f" - {artifact.name}") + + return upload_to_gcs(ctx, artifacts) + + +def upload_signed_artifacts(ctx: BuildContext) -> bool: + """Upload signed artifacts to GCS""" + # For now, this is the same as package artifacts + # Can be extended in the future for specific signed artifacts + return upload_package_artifacts(ctx) + + +def download_from_gcs(bucket_name: str, source_path: str, dest_path: Path, ctx: Optional[BuildContext] = None) -> bool: + """Download a file from GCS (utility function)""" + if not GCS_AVAILABLE: + log_error("google-cloud-storage not installed") + return False + + try: + # Try to use service account if available + client = None + if ctx: + service_account_path = join_paths(ctx.root_dir, SERVICE_ACCOUNT_FILE) + if service_account_path.exists(): + credentials = service_account.Credentials.from_service_account_file( + str(service_account_path) + ) + client = storage.Client(credentials=credentials) + + # Fall back to anonymous client for public buckets + if not client: + client = storage.Client.create_anonymous_client() + + bucket = client.bucket(bucket_name) + blob = bucket.blob(source_path) + + log_info(f"šŸ“„ Downloading gs://{bucket_name}/{source_path}...") + blob.download_to_filename(str(dest_path)) + log_success(f"Downloaded to: {dest_path}") + return True + + except Exception as e: + log_error(f"Failed to download from GCS: {e}") + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6e9b554..c7fa6ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ click>=8.0.0 PyYAML>=5.4.1 requests>=2.25.1 +google-cloud-storage>=2.10.0