124 lines
3.7 KiB
Python
Executable File
124 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Append web viewer assets to a CLI binary built with __trailing_web_viewer feature.
|
|
|
|
This script creates a zip archive of the web viewer assets and appends it to the
|
|
end of a binary, along with metadata needed to locate and extract the assets at runtime.
|
|
|
|
Format of trailing data:
|
|
[Original Binary] [ZIP Archive] [ZIP Offset: 8 bytes LE] [Magic: "RERUNWEB"]
|
|
|
|
Usage:
|
|
python3 scripts/append_web_viewer.py <binary_path> <web_viewer_dir>
|
|
|
|
Example:
|
|
python3 scripts/append_web_viewer.py target/release/rerun rerun/crates/viewer/re_web_viewer_server/web_viewer
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import struct
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
MAGIC = b"RERUNWEB"
|
|
MAGIC_LEN = 8
|
|
OFFSET_LEN = 8
|
|
|
|
|
|
def create_web_viewer_zip(web_viewer_dir: Path) -> bytes:
|
|
"""Create a zip archive of the web viewer assets."""
|
|
required_files = [
|
|
"index.html",
|
|
"favicon.ico",
|
|
"apple-touch-icon.png",
|
|
"sw.js",
|
|
"re_viewer.js",
|
|
"re_viewer_bg.wasm",
|
|
"signed-in.html",
|
|
]
|
|
|
|
# Check that all required files exist
|
|
for filename in required_files:
|
|
file_path = web_viewer_dir / filename
|
|
if not file_path.exists():
|
|
raise FileNotFoundError(f"Required file not found: {file_path}")
|
|
|
|
# Create the zip archive in memory
|
|
zip_buffer = io.BytesIO()
|
|
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
for filename in required_files:
|
|
file_path = web_viewer_dir / filename
|
|
zip_file.write(file_path, arcname=filename)
|
|
|
|
return zip_buffer.getvalue()
|
|
|
|
|
|
def append_web_viewer_to_binary(binary_path: Path, web_viewer_dir: Path) -> None:
|
|
"""Append web viewer assets to a binary."""
|
|
if not binary_path.exists():
|
|
raise FileNotFoundError(f"Binary not found: {binary_path}")
|
|
|
|
if not web_viewer_dir.is_dir():
|
|
raise NotADirectoryError(f"Web viewer directory not found: {web_viewer_dir}")
|
|
|
|
print(f"Creating zip archive from {web_viewer_dir}…")
|
|
zip_data = create_web_viewer_zip(web_viewer_dir)
|
|
print(f"Created zip archive ({len(zip_data)} bytes)")
|
|
|
|
# Get the current size of the binary (this is where the zip will start)
|
|
binary_size = binary_path.stat().st_size
|
|
zip_offset = binary_size
|
|
|
|
print(f"Appending to binary {binary_path} (current size: {binary_size} bytes)…")
|
|
|
|
# Append the zip data, offset, and magic to the binary
|
|
with open(binary_path, "ab") as f:
|
|
# Write the zip archive
|
|
f.write(zip_data)
|
|
|
|
# Write the zip offset (8 bytes, little-endian)
|
|
f.write(struct.pack("<Q", zip_offset))
|
|
|
|
# Write the magic marker
|
|
f.write(MAGIC)
|
|
|
|
new_size = binary_path.stat().st_size
|
|
print(f"Done! New binary size: {new_size} bytes (+{new_size - binary_size} bytes)")
|
|
print()
|
|
print("The binary can now be run and will serve the web viewer from the appended assets.")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Append web viewer assets to a CLI binary.",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
parser.add_argument(
|
|
"binary_path",
|
|
type=Path,
|
|
help="Path to the CLI binary (must be built with __trailing_web_viewer feature)",
|
|
)
|
|
parser.add_argument(
|
|
"web_viewer_dir",
|
|
type=Path,
|
|
help="Path to the web viewer directory containing the assets",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
append_web_viewer_to_binary(args.binary_path, args.web_viewer_dir)
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|