mirror of
https://github.com/katanemo/plano.git
synced 2026-04-25 00:36:34 +02:00
325 lines
11 KiB
Python
325 lines
11 KiB
Python
import gzip
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
|
|
import planoai
|
|
from planoai.consts import (
|
|
ENVOY_VERSION,
|
|
PLANO_BIN_DIR,
|
|
PLANO_PLUGINS_DIR,
|
|
PLANO_RELEASE_BASE_URL,
|
|
)
|
|
from planoai.utils import find_repo_root, getLogger
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
def _get_platform_slug():
|
|
"""Return the platform slug for binary downloads."""
|
|
system = platform.system().lower()
|
|
machine = platform.machine().lower()
|
|
|
|
mapping = {
|
|
("linux", "x86_64"): "linux-amd64",
|
|
("linux", "aarch64"): "linux-arm64",
|
|
("darwin", "arm64"): "darwin-arm64",
|
|
}
|
|
|
|
slug = mapping.get((system, machine))
|
|
if slug is None:
|
|
if system == "darwin" and machine == "x86_64":
|
|
print(
|
|
"Error: macOS x86_64 (Intel) is not supported. "
|
|
"Pre-built binaries are only available for Apple Silicon (arm64)."
|
|
)
|
|
sys.exit(1)
|
|
print(
|
|
f"Error: Unsupported platform {system}/{machine}. "
|
|
"Supported platforms: linux-amd64, linux-arm64, darwin-arm64"
|
|
)
|
|
sys.exit(1)
|
|
|
|
return slug
|
|
|
|
|
|
def _download_file(url, dest, label=None):
|
|
"""Download a file from *url* to *dest* with a progress bar."""
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
if label is None:
|
|
label = os.path.basename(dest)
|
|
|
|
try:
|
|
response = urllib.request.urlopen(url)
|
|
total = int(response.headers.get("Content-Length", 0))
|
|
downloaded = 0
|
|
block_size = 64 * 1024
|
|
|
|
with open(dest, "wb") as f:
|
|
while True:
|
|
chunk = response.read(block_size)
|
|
if not chunk:
|
|
break
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
if total > 0:
|
|
pct = downloaded * 100 // total
|
|
bar_len = 30
|
|
filled = bar_len * downloaded // total
|
|
bar = "█" * filled + "░" * (bar_len - filled)
|
|
mb = downloaded / (1024 * 1024)
|
|
total_mb = total / (1024 * 1024)
|
|
print(
|
|
f"\r {label} {bar} {pct}% ({mb:.1f}/{total_mb:.1f} MB)",
|
|
end="",
|
|
flush=True,
|
|
)
|
|
|
|
print() # newline after progress bar
|
|
|
|
except urllib.error.URLError as e:
|
|
print(f"\nError downloading {label}: {e}")
|
|
print(f" URL: {url}")
|
|
print("Please check your internet connection and try again.")
|
|
sys.exit(1)
|
|
|
|
|
|
def ensure_envoy_binary():
|
|
"""Download Envoy binary if not already present or version changed. Returns path to binary."""
|
|
envoy_path = os.path.join(PLANO_BIN_DIR, "envoy")
|
|
version_path = os.path.join(PLANO_BIN_DIR, "envoy.version")
|
|
|
|
if os.path.exists(envoy_path) and os.access(envoy_path, os.X_OK):
|
|
# Check if cached binary matches the pinned version
|
|
if os.path.exists(version_path):
|
|
with open(version_path, "r") as f:
|
|
cached_version = f.read().strip()
|
|
if cached_version == ENVOY_VERSION:
|
|
log.info(f"Envoy {ENVOY_VERSION} (cached)")
|
|
return envoy_path
|
|
log.info(
|
|
f"Envoy version changed ({cached_version} → {ENVOY_VERSION}), re-downloading..."
|
|
)
|
|
else:
|
|
log.info("Envoy binary found (unknown version, re-downloading...)")
|
|
|
|
slug = _get_platform_slug()
|
|
url = (
|
|
f"https://github.com/tetratelabs/archive-envoy/releases/download/"
|
|
f"{ENVOY_VERSION}/envoy-{ENVOY_VERSION}-{slug}.tar.xz"
|
|
)
|
|
|
|
os.makedirs(PLANO_BIN_DIR, exist_ok=True)
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".tar.xz", delete=False) as tmp:
|
|
tmp_path = tmp.name
|
|
|
|
try:
|
|
_download_file(url, tmp_path, label=f"Envoy {ENVOY_VERSION}")
|
|
log.info(f"Extracting Envoy {ENVOY_VERSION}...")
|
|
with tarfile.open(tmp_path, "r:xz") as tar:
|
|
# Find the envoy binary inside the archive
|
|
envoy_member = None
|
|
for member in tar.getmembers():
|
|
if member.name.endswith("/bin/envoy") or member.name == "bin/envoy":
|
|
envoy_member = member
|
|
break
|
|
|
|
if envoy_member is None:
|
|
print("Error: Could not find envoy binary in the downloaded archive.")
|
|
print("Archive contents:")
|
|
for member in tar.getmembers():
|
|
print(f" {member.name}")
|
|
sys.exit(1)
|
|
|
|
# Extract just the binary
|
|
f = tar.extractfile(envoy_member)
|
|
if f is None:
|
|
print("Error: Could not extract envoy binary from archive.")
|
|
sys.exit(1)
|
|
|
|
with open(envoy_path, "wb") as out:
|
|
out.write(f.read())
|
|
|
|
os.chmod(envoy_path, 0o755)
|
|
with open(version_path, "w") as f:
|
|
f.write(ENVOY_VERSION)
|
|
return envoy_path
|
|
|
|
finally:
|
|
if os.path.exists(tmp_path):
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def _find_local_wasm_plugins():
|
|
"""Check for WASM plugins built from source. Returns (prompt_gw, llm_gw) or None."""
|
|
repo_root = find_repo_root()
|
|
if not repo_root:
|
|
return None
|
|
wasm_dir = os.path.join(repo_root, "crates", "target", "wasm32-wasip1", "release")
|
|
prompt_gw = os.path.join(wasm_dir, "prompt_gateway.wasm")
|
|
llm_gw = os.path.join(wasm_dir, "llm_gateway.wasm")
|
|
if os.path.exists(prompt_gw) and os.path.exists(llm_gw):
|
|
return prompt_gw, llm_gw
|
|
return None
|
|
|
|
|
|
def _find_local_brightstaff():
|
|
"""Check for brightstaff binary built from source. Returns path or None."""
|
|
repo_root = find_repo_root()
|
|
if not repo_root:
|
|
return None
|
|
path = os.path.join(repo_root, "crates", "target", "release", "brightstaff")
|
|
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
return path
|
|
return None
|
|
|
|
|
|
def ensure_wasm_plugins():
|
|
"""Find or download WASM plugins. Checks: local build → cached download → fresh download."""
|
|
# 1. Local source build (inside repo)
|
|
local = _find_local_wasm_plugins()
|
|
if local:
|
|
log.info("Using locally-built WASM plugins")
|
|
return local
|
|
|
|
# 2. Cached download
|
|
version = planoai.__version__
|
|
version_path = os.path.join(PLANO_PLUGINS_DIR, "wasm.version")
|
|
prompt_gw_path = os.path.join(PLANO_PLUGINS_DIR, "prompt_gateway.wasm")
|
|
llm_gw_path = os.path.join(PLANO_PLUGINS_DIR, "llm_gateway.wasm")
|
|
|
|
if os.path.exists(prompt_gw_path) and os.path.exists(llm_gw_path):
|
|
if os.path.exists(version_path):
|
|
with open(version_path, "r") as f:
|
|
cached_version = f.read().strip()
|
|
if cached_version == version:
|
|
log.info(f"WASM plugins {version} (cached)")
|
|
return prompt_gw_path, llm_gw_path
|
|
log.info(
|
|
f"WASM plugins version changed ({cached_version} → {version}), re-downloading..."
|
|
)
|
|
else:
|
|
log.info("WASM plugins found (unknown version, re-downloading...)")
|
|
|
|
# 3. Download from GitHub releases (gzipped)
|
|
os.makedirs(PLANO_PLUGINS_DIR, exist_ok=True)
|
|
|
|
for name, dest in [
|
|
("prompt_gateway.wasm", prompt_gw_path),
|
|
("llm_gateway.wasm", llm_gw_path),
|
|
]:
|
|
gz_name = f"{name}.gz"
|
|
url = f"{PLANO_RELEASE_BASE_URL}/{version}/{gz_name}"
|
|
gz_dest = dest + ".gz"
|
|
_download_file(url, gz_dest, label=f"{name} ({version})")
|
|
log.info(f"Decompressing {name}...")
|
|
with gzip.open(gz_dest, "rb") as f_in, open(dest, "wb") as f_out:
|
|
shutil.copyfileobj(f_in, f_out)
|
|
os.unlink(gz_dest)
|
|
|
|
with open(version_path, "w") as f:
|
|
f.write(version)
|
|
|
|
return prompt_gw_path, llm_gw_path
|
|
|
|
|
|
def ensure_brightstaff_binary():
|
|
"""Find or download brightstaff binary. Checks: local build → cached download → fresh download."""
|
|
# 1. Local source build (inside repo)
|
|
local = _find_local_brightstaff()
|
|
if local:
|
|
log.info("Using locally-built brightstaff")
|
|
return local
|
|
|
|
# 2. Cached download
|
|
version = planoai.__version__
|
|
brightstaff_path = os.path.join(PLANO_BIN_DIR, "brightstaff")
|
|
version_path = os.path.join(PLANO_BIN_DIR, "brightstaff.version")
|
|
|
|
if os.path.exists(brightstaff_path) and os.access(brightstaff_path, os.X_OK):
|
|
if os.path.exists(version_path):
|
|
with open(version_path, "r") as f:
|
|
cached_version = f.read().strip()
|
|
if cached_version == version:
|
|
log.info(f"brightstaff {version} (cached)")
|
|
return brightstaff_path
|
|
log.info(
|
|
f"brightstaff version changed ({cached_version} → {version}), re-downloading..."
|
|
)
|
|
else:
|
|
log.info("brightstaff found (unknown version, re-downloading...)")
|
|
|
|
# 3. Download from GitHub releases (gzipped)
|
|
slug = _get_platform_slug()
|
|
filename = f"brightstaff-{slug}.gz"
|
|
url = f"{PLANO_RELEASE_BASE_URL}/{version}/{filename}"
|
|
|
|
os.makedirs(PLANO_BIN_DIR, exist_ok=True)
|
|
|
|
gz_path = brightstaff_path + ".gz"
|
|
_download_file(url, gz_path, label=f"brightstaff ({version}, {slug})")
|
|
log.info("Decompressing brightstaff...")
|
|
with gzip.open(gz_path, "rb") as f_in, open(brightstaff_path, "wb") as f_out:
|
|
shutil.copyfileobj(f_in, f_out)
|
|
os.unlink(gz_path)
|
|
|
|
os.chmod(brightstaff_path, 0o755)
|
|
with open(version_path, "w") as f:
|
|
f.write(version)
|
|
return brightstaff_path
|
|
|
|
|
|
def find_wasm_plugins():
|
|
"""Find WASM plugin files built from source. Returns (prompt_gateway_path, llm_gateway_path)."""
|
|
repo_root = find_repo_root()
|
|
if not repo_root:
|
|
print(
|
|
"Error: Could not find repository root. "
|
|
"Make sure you're inside the plano repository."
|
|
)
|
|
sys.exit(1)
|
|
|
|
wasm_dir = os.path.join(repo_root, "crates", "target", "wasm32-wasip1", "release")
|
|
prompt_gw = os.path.join(wasm_dir, "prompt_gateway.wasm")
|
|
llm_gw = os.path.join(wasm_dir, "llm_gateway.wasm")
|
|
|
|
missing = []
|
|
if not os.path.exists(prompt_gw):
|
|
missing.append("prompt_gateway.wasm")
|
|
if not os.path.exists(llm_gw):
|
|
missing.append("llm_gateway.wasm")
|
|
|
|
if missing:
|
|
print(f"Error: WASM plugins not found: {', '.join(missing)}")
|
|
print(f" Expected at: {wasm_dir}/")
|
|
print(" Run 'planoai build' first to build them.")
|
|
sys.exit(1)
|
|
|
|
return prompt_gw, llm_gw
|
|
|
|
|
|
def find_brightstaff_binary():
|
|
"""Find the brightstaff binary built from source. Returns path."""
|
|
repo_root = find_repo_root()
|
|
if not repo_root:
|
|
print(
|
|
"Error: Could not find repository root. "
|
|
"Make sure you're inside the plano repository."
|
|
)
|
|
sys.exit(1)
|
|
|
|
brightstaff_path = os.path.join(
|
|
repo_root, "crates", "target", "release", "brightstaff"
|
|
)
|
|
if not os.path.exists(brightstaff_path):
|
|
print(f"Error: brightstaff binary not found at {brightstaff_path}")
|
|
print(" Run 'planoai build' first to build it.")
|
|
sys.exit(1)
|
|
|
|
return brightstaff_path
|