Skip to content

Commit a338744

Browse files
authored
chore(ready-to-run): download ready-to-run bins from release (#973)
Previously, we use submodule to manage all ready-to-run binaries, which comes a problem after update ready-to-run again and again. The ready-to-run repo became huge and difficult to download. This patch imports a script and a config file to download ready-to-runs. This patch is assisted with AI.
1 parent e47e0a6 commit a338744

File tree

8 files changed

+229
-6
lines changed

8 files changed

+229
-6
lines changed

.github/workflows/ci.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ on:
88

99
env:
1010
CI_WORKLOADS: ./workloads
11-
SPIKE_SO: ./ready-to-run/riscv64-spike-so
12-
NUTSHELL_SPIKE_SO: ./ready-to-run/riscv64-nutshell-spike-so
11+
SPIKE_SO: ./ready-to-run/spike-xiangshan-ref.so
12+
NUTSHELL_SPIKE_SO: ./ready-to-run/spike-nutshell-ref.so
1313
CROSS_COMPILE: riscv64-linux-gnu-
1414
LIBCHECKPOINT_CROSS_COMPILE: riscv64-linux-gnu-
1515
CPT_CROSS_COMPILE: riscv64-linux-gnu-
@@ -109,6 +109,9 @@ jobs:
109109
- name: Setup env
110110
run: |
111111
echo "NEMU_HOME=$GITHUB_WORKSPACE" >> $GITHUB_ENV
112+
- name: Initialize ready-to-run
113+
run: |
114+
make init-ready-to-run
112115
- name: Build NEMU interpreter for XS
113116
run: |
114117
make riscv64-xs_defconfig
@@ -168,6 +171,9 @@ jobs:
168171
- name: Setup env
169172
run: |
170173
echo "NEMU_HOME=$GITHUB_WORKSPACE" >> $GITHUB_ENV
174+
- name: Initialize ready-to-run
175+
run: |
176+
make init-ready-to-run
171177
- name: Build NEMU interpreter
172178
run: |
173179
make riscv64-nutshell_defconfig
@@ -204,6 +210,10 @@ jobs:
204210
run: |
205211
echo "NEMU_HOME=$GITHUB_WORKSPACE" >> $GITHUB_ENV
206212
213+
- name: Initialize ready-to-run
214+
run: |
215+
make init-ready-to-run
216+
207217
- name: Build NEMU with V extension and agnostic
208218
run: |
209219
make riscv64-xs-diff-spike-agnostic_defconfig
@@ -244,6 +254,10 @@ jobs:
244254
run: |
245255
echo "NEMU_HOME=$GITHUB_WORKSPACE" >> $GITHUB_ENV
246256
257+
- name: Initialize ready-to-run
258+
run: |
259+
make init-ready-to-run
260+
247261
- name: Build NEMU interpreter for diff with spike
248262
run: |
249263
make riscv64-xs-diff-spike_defconfig
@@ -357,6 +371,10 @@ jobs:
357371
run: |
358372
echo "NEMU_HOME=$GITHUB_WORKSPACE" >> $GITHUB_ENV
359373
374+
- name: Initialize ready-to-run
375+
run: |
376+
make init-ready-to-run
377+
360378
- name: Build NEMU interpreter for diff with spike
361379
run: |
362380
make clean-all

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
!*.[cSh]
77
!*.cpp
88
!*.lds
9+
!*.yml
910
!.gitignore
1011
!Dockerfile
1112
!README.md
1213
!Kconfig
1314
!.github/workflows/*.yml
1415
!scripts/*.sh
16+
!scripts/*.py
1517
!scripts/checkpoint_example/*
1618
include/config
1719
include/generated

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
[submodule "ready-to-run"]
2-
path = ready-to-run
3-
url = https://github.com/OpenXiangShan/ready-to-run.git
41
[submodule "resource/simpoint/simpoint_repo"]
52
path = resource/simpoint/simpoint_repo
63
url = https://github.com/shinezyy/SimPoint.3.2-fix.git

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ LDFLAGS += -lm
127127
endif
128128

129129
include $(NEMU_HOME)/scripts/repos.mk
130+
include $(NEMU_HOME)/scripts/ready-to-run.mk
130131
include $(NEMU_HOME)/scripts/git.mk
131132
include $(NEMU_HOME)/scripts/config.mk
132133
include $(NEMU_HOME)/scripts/isa.mk

ready-to-run

Lines changed: 0 additions & 1 deletion
This file was deleted.

ready-to-run.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
spike:
2+
type: github-release
3+
repo: OpenXiangShan/riscv-isa-sim
4+
version: v2025.12.r2
5+
assets:
6+
- save_as: spike-xiangshan-ref.so
7+
asset_name: spike-{version}-xiangshan-ref.so
8+
- save_as: spike-nutshell-ref.so
9+
asset_name: spike-{version}-nutshell-ref.so
10+

scripts/init-ready-to-run.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import base64
5+
import binascii
6+
import hashlib
7+
import json
8+
from pathlib import Path
9+
from typing import Any, Dict, Iterable, Optional, Tuple
10+
import urllib.error
11+
import urllib.request
12+
13+
try:
14+
import yaml
15+
except ImportError as exc: # pragma: no cover
16+
raise SystemExit("Missing dependency: PyYAML is required (pip install pyyaml)") from exc
17+
18+
GITHUB_API_ROOT = "https://api.github.com"
19+
USER_AGENT = "init-ready-to-run/1.0"
20+
21+
22+
def load_config(config_path: Path) -> Dict[str, Any]:
23+
with config_path.open("r", encoding="utf-8") as handle:
24+
data = yaml.safe_load(handle) or {}
25+
if not isinstance(data, dict):
26+
raise ValueError("Top-level YAML structure must be a mapping")
27+
return data
28+
29+
30+
def github_request(url: str) -> Dict[str, Any]:
31+
headers = {"Accept": "application/vnd.github+json", "User-Agent": USER_AGENT}
32+
request = urllib.request.Request(url, headers=headers)
33+
try:
34+
with urllib.request.urlopen(request) as response:
35+
payload = response.read().decode("utf-8")
36+
except urllib.error.HTTPError as error: # pragma: no cover
37+
detail = error.read().decode("utf-8", errors="ignore") if error.fp else ""
38+
raise RuntimeError(f"GitHub request failed ({error.code}): {detail}") from error
39+
return json.loads(payload)
40+
41+
42+
def resolve_asset(asset_cfg: Dict[str, Any], release_version: str) -> Tuple[str, str]:
43+
remote_template = asset_cfg.get("asset_name")
44+
if not remote_template:
45+
raise ValueError("Asset entry must define 'asset_name'")
46+
remote_name = remote_template.format(version=release_version)
47+
target_name = asset_cfg.get("save_as") or remote_name
48+
return remote_name, target_name
49+
50+
51+
def find_release_asset(release: Dict[str, Any], asset_name: str) -> Dict[str, Any]:
52+
for asset in release.get("assets", []):
53+
if asset.get("name") == asset_name:
54+
return asset
55+
available = ", ".join(asset.get("name", "<unknown>") for asset in release.get("assets", []))
56+
raise FileNotFoundError(f"Asset '{asset_name}' not found in release; available: {available}")
57+
58+
59+
def download_asset(url: str, destination: Path) -> None:
60+
headers = {"User-Agent": USER_AGENT}
61+
request = urllib.request.Request(url, headers=headers)
62+
destination.parent.mkdir(parents=True, exist_ok=True)
63+
with urllib.request.urlopen(request) as response, destination.open("wb") as output:
64+
while True:
65+
chunk = response.read(1024 * 1024)
66+
if not chunk:
67+
break
68+
output.write(chunk)
69+
70+
71+
def parse_asset_digest(asset: Dict[str, Any]) -> Optional[Tuple[str, str]]:
72+
digest = asset.get("digest")
73+
if not digest:
74+
return None
75+
if ":" not in digest:
76+
raise ValueError(f"Unsupported digest format '{digest}'")
77+
algorithm, expected = digest.split(":", 1)
78+
algorithm = algorithm.strip().lower()
79+
expected = expected.strip()
80+
if not algorithm:
81+
raise ValueError(f"Missing digest algorithm in '{digest}'")
82+
if algorithm not in hashlib.algorithms_available:
83+
raise ValueError(f"Unsupported digest algorithm '{algorithm}'")
84+
return algorithm, expected
85+
86+
87+
def verify_digest(path: Path, algorithm: str, expected: str) -> bool:
88+
digest = hashlib.new(algorithm)
89+
with path.open("rb") as handle:
90+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
91+
digest.update(chunk)
92+
actual_bytes = digest.digest()
93+
actual_hex = digest.hexdigest()
94+
if expected.lower() == actual_hex:
95+
return True
96+
try:
97+
expected_bytes = bytes.fromhex(expected)
98+
if expected_bytes == actual_bytes:
99+
return True
100+
except ValueError:
101+
pass
102+
try:
103+
expected_bytes = base64.b64decode(expected, validate=True)
104+
except binascii.Error:
105+
return False
106+
return expected_bytes == actual_bytes
107+
108+
109+
def process_github_release(artifact_id: str, cfg: Dict[str, Any], output_dir: Path, force: bool) -> None:
110+
repo = cfg.get("repo")
111+
version = cfg.get("version")
112+
assets = cfg.get("assets")
113+
if not repo or not version or not assets:
114+
raise ValueError(f"Entry '{artifact_id}' must define repo, version, and assets")
115+
116+
release_url = f"{GITHUB_API_ROOT}/repos/{repo}/releases/tags/{version}"
117+
release = github_request(release_url)
118+
119+
for asset_cfg in ensure_iterable(assets):
120+
remote_name, target_name = resolve_asset(asset_cfg, version)
121+
destination = output_dir / target_name
122+
123+
asset = find_release_asset(release, remote_name)
124+
digest_info = parse_asset_digest(asset)
125+
126+
if destination.exists() and not force:
127+
if digest_info:
128+
algorithm, expected = digest_info
129+
if verify_digest(destination, algorithm, expected):
130+
print(f"[skip] {artifact_id}: {target_name} already exists")
131+
continue
132+
print(f"[redo] {artifact_id}: {target_name} digest mismatch, re-downloading")
133+
else:
134+
print(f"[skip] {artifact_id}: {target_name} already exists (no digest)")
135+
continue
136+
137+
url = asset.get("browser_download_url")
138+
if not url:
139+
raise RuntimeError(f"Asset '{remote_name}' missing browser_download_url")
140+
141+
if destination.exists():
142+
destination.unlink()
143+
144+
print(f"[download] {artifact_id}: {remote_name} -> {destination.relative_to(output_dir)}")
145+
download_asset(url, destination)
146+
147+
if digest_info and not verify_digest(destination, *digest_info):
148+
destination.unlink(missing_ok=True)
149+
raise ValueError(f"Digest mismatch for {destination}")
150+
151+
152+
TYPE_HANDLERS = {
153+
"github-release": process_github_release,
154+
}
155+
156+
157+
def ensure_iterable(value: Any) -> Iterable[Any]:
158+
if isinstance(value, list):
159+
return value
160+
return [value]
161+
162+
163+
def process_entries(config: Dict[str, Any], output_dir: Path, force: bool) -> None:
164+
for artifact_id, cfg in config.items():
165+
entry_type = cfg.get("type")
166+
handler = TYPE_HANDLERS.get(entry_type)
167+
if not handler:
168+
print(f"[skip] {artifact_id}: unsupported type '{entry_type}'")
169+
continue
170+
handler(artifact_id, cfg, output_dir, force)
171+
172+
173+
def parse_args() -> argparse.Namespace:
174+
root = Path(__file__).resolve().parent.parent
175+
parser = argparse.ArgumentParser(description="Initialize ready-to-run assets from release definitions")
176+
parser.add_argument("--config", default=str(root / "ready-to-run.yml"), help="Path to YAML configuration file")
177+
parser.add_argument("--output", default=str(root / "ready-to-run"), help="Directory where assets are stored")
178+
parser.add_argument("--force", action="store_true", help="Re-download assets even if they already exist")
179+
return parser.parse_args()
180+
181+
182+
def main() -> None:
183+
args = parse_args()
184+
config_path = Path(args.config)
185+
output_dir = Path(args.output)
186+
187+
config = load_config(config_path)
188+
process_entries(config, output_dir, args.force)
189+
190+
191+
if __name__ == "__main__":
192+
main()

scripts/ready-to-run.mk

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
init-ready-to-run:
2+
@python3 $(NEMU_HOME)/scripts/init-ready-to-run.py
3+
4+
.PHONY: init-ready-to-run

0 commit comments

Comments
 (0)