Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 82 additions & 60 deletions gltest/direct/sdk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,36 @@
import re
import sys
import json
import shutil
import tarfile
import platform
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
from typing import Optional, Dict, List

CACHE_DIR = Path.home() / ".cache" / "gltest-direct"
GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm/releases"
GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm/releases"

# GenVM 0.3.0 renamed this bundle from genvm-universal.tar.xz; newest name first.
RUNNER_BUNDLE_ASSETS = ("genvm-runners-all.tar.xz", "genvm-universal.tar.xz")
GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm-manager/releases"
GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm-manager/releases"


def _host_release_asset() -> str:
"""genvm-manager (v0.6+) ships a whole-tree tarball per platform, named
genvm-<arch>-<os>.tar.xz — pick the one matching this host."""
machine = platform.machine().lower()
arch = "arm64" if machine in ("aarch64", "arm64") else "amd64"
os_name = "macos" if platform.system().lower() == "darwin" else "linux"
return f"genvm-{arch}-{os_name}.tar.xz"


# Download candidates, newest-scheme first: the genvm-manager per-platform whole
# tree, then the pre-v0.6 runner-only bundles (genvm-runners-all → genvm-universal).
RUNNER_BUNDLE_ASSETS = (
_host_release_asset(),
"genvm-runners-all.tar.xz",
"genvm-universal.tar.xz",
)
GENVM_VERSION_ENV = "GENVM_VERSION"
FALLBACK_VERSION = "v0.2.16"

Expand Down Expand Up @@ -158,68 +175,72 @@ def download_artifacts(version: str) -> Path:
) from last_error


def _extract_local_runner(
root: Path, runner_type: str, runner_hash: Optional[str]
) -> Path:
"""Extract a runner from a local prebuilt GenVM tree (GENVM_PREBUILT_DIR); globs
any *runners* dir so runners/ and executor/<ver>/legacy-runners/ both match."""
sub = (
f"{runner_hash[:2]}/{runner_hash[2:]}.tar"
if runner_hash and runner_hash.lower() != "latest"
else "*/*.tar"
)
hits = sorted(root.glob(f"**/*runners*/{runner_type}/{sub}"))
if not hits:
raise FileNotFoundError(f"runner {runner_type}:{runner_hash} not under {root}")
tar = hits[-1]
dest = CACHE_DIR / "extracted" / "local" / runner_type / (tar.parent.name + tar.stem)
if not dest.exists():
dest.mkdir(parents=True, exist_ok=True)
with tarfile.open(tar, "r:") as inner:
inner.extractall(dest, filter="data")
return dest


def _extract_release_tree(tarball_path: Path, version: str) -> Path:
"""Unpack a downloaded GenVM release tarball once (cached) into a local tree.

genvm-manager ships the whole tree (bin/ lib/ runners/ executor/…), not a
runner-only bundle, so we unpack it to a directory that looks exactly like a
GENVM_PREBUILT_DIR and then resolve runners through the same globbing path.
"""
tree = CACHE_DIR / "trees" / version
if (tree / ".extracted").exists():
return tree
trees = CACHE_DIR / "trees"
trees.mkdir(parents=True, exist_ok=True)
# Extract into a process-unique dir, mark it complete, then publish with an
# atomic rename. Concurrent cold-cache extractions each use their own tmp and
# race only on the final rename; the loser sees the winner's finished tree.
tmp = Path(tempfile.mkdtemp(dir=trees, prefix=f".{version}."))
try:
with tarfile.open(tarball_path, "r:xz") as outer:
outer.extractall(tmp, filter="data")
(tmp / ".extracted").touch()
os.replace(tmp, tree)
except OSError:
shutil.rmtree(tmp, ignore_errors=True)
if (tree / ".extracted").exists():
return tree # another extraction published first
raise
return tree


def extract_runner(
tarball_path: Path,
runner_type: str,
runner_hash: Optional[str] = None,
version: Optional[str] = None,
) -> Path:
"""Extract a runner from the tarball."""
"""Resolve a runner dir from a local prebuilt tree or a downloaded release."""
prebuilt = os.environ.get("GENVM_PREBUILT_DIR")
if prebuilt:
return _extract_local_runner(Path(prebuilt), runner_type, runner_hash)
if version is None:
match = re.search(r"genvm-universal-(.+)\.tar\.xz", tarball_path.name)
version = match.group(1) if match else "unknown"

extract_base = CACHE_DIR / "extracted" / version / runner_type

# Fast path: if hash specified and already extracted, skip tarball entirely
if runner_hash and runner_hash.lower() != "latest":
extract_dir = extract_base / runner_hash
if extract_dir.exists():
return extract_dir

# Check if any version already extracted (for "latest" case)
if extract_base.exists():
existing = sorted(extract_base.iterdir(), reverse=True)
if existing and (not runner_hash or runner_hash.lower() == "latest"):
return existing[0]

# Need to open tarball - this is slow (~13s for xz)
with tarfile.open(tarball_path, "r:xz") as outer_tar:
prefix = f"runners/{runner_type}/"
runner_tars = [
m.name for m in outer_tar.getmembers()
if m.name.startswith(prefix) and m.name.endswith(".tar")
]

if not runner_tars:
raise ValueError(f"No {runner_type} runners found in tarball")

# Treat "latest" as no specific hash
if runner_hash and runner_hash.lower() != "latest":
target = f"runners/{runner_type}/{runner_hash[:2]}/{runner_hash[2:]}.tar"
if target not in runner_tars:
raise ValueError(f"Runner hash {runner_hash} not found")
runner_tar_name = target
extract_dir = extract_base / runner_hash
else:
runner_tar_name = sorted(runner_tars)[-1]
parts = runner_tar_name.split("/")
runner_hash = parts[-2] + parts[-1].replace(".tar", "")
extract_dir = extract_base / runner_hash

if extract_dir.exists():
return extract_dir

inner_tar_file = outer_tar.extractfile(runner_tar_name)
if inner_tar_file is None:
raise ValueError(f"Failed to read {runner_tar_name}")

extract_dir.mkdir(parents=True, exist_ok=True)

with tarfile.open(fileobj=inner_tar_file, mode="r:") as inner_tar:
inner_tar.extractall(extract_dir, filter='data')

return extract_dir
tree = _extract_release_tree(tarball_path, version)
return _extract_local_runner(tree, runner_type, runner_hash)


def parse_runner_manifest(runner_dir: Path) -> Dict[str, str]:
Expand Down Expand Up @@ -256,10 +277,11 @@ def setup_sdk_paths(
if contract_path and contract_path.exists():
contract_deps = parse_contract_header(contract_path)

if version is None:
prebuilt = os.environ.get("GENVM_PREBUILT_DIR")
if version is None and not prebuilt:
version = resolve_version()

tarball = download_artifacts(version)
tarball = None if prebuilt else download_artifacts(version)

runner_hash = contract_deps.get(RUNNER_TYPE)
runner_dir = extract_runner(tarball, RUNNER_TYPE, runner_hash, version)
Expand Down
2 changes: 1 addition & 1 deletion tests/examples/contracts/football_prediction_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, game_date: str, team1: str, team2: str):
)
self.team1 = team1
self.team2 = team2
self.winner = gl.u256(0)
self.winner = 0
self.score = ""

@gl.public.write
Expand Down
8 changes: 4 additions & 4 deletions tests/examples/contracts/intelligent_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ class IntelligentOracle(gl.contract.Contract):
prediction_market_id: str
title: str
description: str
potential_outcomes: gl.DynArray[str]
rules: gl.DynArray[str]
data_source_domains: gl.DynArray[str]
resolution_urls: gl.DynArray[str]
potential_outcomes: gl.storage.DynArray[str]
rules: gl.storage.DynArray[str]
data_source_domains: gl.storage.DynArray[str]
resolution_urls: gl.storage.DynArray[str]
earliest_resolution_date: str # Store as ISO format string
status: str # Store as string since Enum isn't supported
analysis: str # Store analysis results
Expand Down
2 changes: 1 addition & 1 deletion tests/examples/contracts/intelligent_oracle_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class Registry(gl.contract.Contract):
# Declare persistent storage fields
contract_addresses: gl.DynArray[str]
contract_addresses: gl.storage.DynArray[str]
intelligent_oracle_code: str

def __init__(self, intelligent_oracle_code: str):
Expand Down
4 changes: 2 additions & 2 deletions tests/examples/contracts/llm_erc20.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@


class LlmErc20(gl.contract.Contract):
balances: gl.TreeMap[gl.Address, gl.u256]
balances: gl.storage.TreeMap[gl.Address, gl.u256]

def __init__(self, total_supply: int) -> None:
self.balances[gl.message.sender_address] = gl.u256(total_supply)
self.balances[gl.message.sender_address] = total_supply

@gl.public.write
def transfer(self, amount: int, to_address: str) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/examples/contracts/log_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ def get_closest_vector(self, text: str) -> dict | None:
@gl.public.write
def add_log(self, log: str, log_id: int) -> None:
emb = self.get_embedding(log)
self.vector_store.insert(emb, StoreValue(text=log, log_id=gl.u256(log_id)))
self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id))

@gl.public.write
def update_log(self, log_id: int, log: str) -> None:
emb = self.get_embedding(log)
for elem in self.vector_store.knn(emb, 2):
if elem.value.text == log:
elem.value.log_id = gl.u256(log_id)
elem.value.log_id = log_id

@gl.public.write
def remove_log(self, id: int) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/examples/contracts/multi_file_contract/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def __init__(self):
self.other_addr = gl.contract.deploy(
code=text.encode("utf-8"),
args=["123"],
salt_nonce=gl.u256(1),
value=gl.u256(0),
salt_nonce=1,
value=0,
on="accepted",
)

Expand Down
2 changes: 1 addition & 1 deletion tests/examples/contracts/multi_read_erc20.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class multi_read_erc20(gl.contract.Contract):
balances: gl.TreeMap[gl.Address, gl.TreeMap[gl.Address, gl.u256]]
balances: gl.storage.TreeMap[gl.Address, gl.storage.TreeMap[gl.Address, gl.u256]]

def __init__(self):
pass
Expand Down
6 changes: 3 additions & 3 deletions tests/examples/contracts/multi_tenant_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class MultiTentantStorage(gl.contract.Contract):
This is done to test contract calls between different contracts.
"""

all_storage_contracts: gl.DynArray[gl.Address]
available_storage_contracts: gl.DynArray[gl.Address]
mappings: gl.TreeMap[
all_storage_contracts: gl.storage.DynArray[gl.Address]
available_storage_contracts: gl.storage.DynArray[gl.Address]
mappings: gl.storage.TreeMap[
gl.Address, gl.Address
] # mapping of user address to storage contract address

Expand Down
2 changes: 1 addition & 1 deletion tests/examples/contracts/user_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class UserStorage(gl.contract.Contract):
storage: gl.TreeMap[gl.Address, str]
storage: gl.storage.TreeMap[gl.Address, str]

# constructor
def __init__(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/glsim/deterministic_factory_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def deploy_child(self, salt: int) -> str:
code=CHILD_CODE.encode("utf-8"),
args=[],
kwargs={},
salt_nonce=gl.u256(salt),
salt_nonce=salt,
on="accepted",
)
self.child_address = child_address.as_hex
Expand Down
2 changes: 1 addition & 1 deletion tests/gltest/artifact/contracts/current_style_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import genlayer as gl


class CurrentStyleContract(gl.Contract):
class CurrentStyleContract(gl.contract.Contract):
storage: str

def __init__(self, initial_storage: str):
Expand Down
5 changes: 3 additions & 2 deletions tests/gltest_direct/test_sdk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def _download(url, dest):
assert result == tmp_path / "genvm-universal-v0.3.0.tar.xz"
assert result.read_bytes() == b"bundle"
assert tried == [
f"{sdk_loader.GITHUB_RELEASES_URL}/download/v0.3.0/genvm-runners-all.tar.xz"
f"{sdk_loader.GITHUB_RELEASES_URL}/download/v0.3.0/{sdk_loader.RUNNER_BUNDLE_ASSETS[0]}"
]

def test_falls_back_to_old_asset_on_404(self, monkeypatch, tmp_path):
Expand All @@ -171,7 +171,8 @@ def test_falls_back_to_old_asset_on_404(self, monkeypatch, tmp_path):

def _download(url, dest):
tried.append(url)
if url.endswith("genvm-runners-all.tar.xz"):
# 404 every asset except the last so the loop walks the full list.
if not url.endswith(sdk_loader.RUNNER_BUNDLE_ASSETS[-1]):
raise self._http_404(url)
dest.write_bytes(b"bundle")

Expand Down
Loading