Skip to content

Commit 433443c

Browse files
author
ssjia
committed
Update (base update)
[ghstack-poisoned]
2 parents aae1a6f + e2bc39b commit 433443c

195 files changed

Lines changed: 22892 additions & 1223 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ci/docker/common/install_zephyr.sh

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
#!/bin/bash
32
# Copyright (c) Meta Platforms, Inc. and affiliates.
43
# All rights reserved.
@@ -9,27 +8,27 @@
98

109
set -ex
1110

11+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
1213
# shellcheck source=/dev/null
13-
source "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
14+
source "${SCRIPT_DIR}/utils.sh"
1415

15-
# Double check if the NDK version is set
16+
# Double check if Zephyr setup is enabled.
1617
[ -n "${ZEPHYR_SDK}" ]
1718

18-
install_prerequiresites() {
19+
install_prerequisites() {
1920
rm /var/lib/dpkg/info/libc-bin.*
2021
apt-get clean
2122
apt-get -y update
2223
apt-get install -y libc-bin
2324
apt-get -y update
2425
apt-get clean
25-
apt-get install --no-install-recommends -y dos2unix
2626
apt-get install --no-install-recommends -y ca-certificates
2727
apt-get install -y --reinstall libc-bin
2828
apt-get install --no-install-recommends -y file
2929
apt-get install --no-install-recommends -y locales
3030
apt-get install --no-install-recommends -y git
3131
apt-get install --no-install-recommends -y build-essential
32-
apt-get install --no-install-recommends -y cmake
3332
apt-get install --no-install-recommends -y ninja-build gperf
3433
apt-get install --no-install-recommends -y device-tree-compiler
3534
apt-get install --no-install-recommends -y wget
@@ -49,16 +48,14 @@ install_prerequiresites() {
4948
apt install -y python3.12 python3.12-dev python3.12-venv python3-pip
5049
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1
5150

52-
# Upgrade cmake ot 3.24
53-
apt update
54-
apt install cmake
55-
apt install software-properties-common lsb-release
51+
# Upgrade cmake to 3.24 or newer.
52+
apt install -y software-properties-common lsb-release
5653
apt update
5754
test -f /usr/share/doc/kitware-archive-keyring/copyright || \
5855
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
59-
"deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/kitware.list > /dev/null
56+
echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/kitware.list > /dev/null
6057
apt update
61-
apt install cmake
58+
apt install -y cmake
6259

6360
# Install additional required software for Zephyr
6461
apt install --no-install-recommends -y ccache \
@@ -77,19 +74,19 @@ install_prerequiresites() {
7774
apt-get clean -y
7875
apt-get autoremove --purge -y
7976
rm -rf /var/lib/apt/lists/*
80-
wget https://apt.kitware.com/kitware-archive.sh && \
81-
chmod +x kitware-archive.sh && \
82-
./kitware-archive.sh && \
83-
rm -f kitware-archive.sh
8477
pip_install --no-cache-dir west
8578
pip_install pyelftools
8679

87-
# Zephyr SDK
88-
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.17.4/zephyr-sdk-0.17.4_linux-x86_64.tar.xz
89-
tar -xf zephyr-sdk-0.17.4_linux-x86_64.tar.xz
90-
rm -f zephyr-sdk-0.17.4_linux-x86_64.tar.xz*
91-
# Save setup to later and this get symlinked in to another folder in the test in trunk.yml
92-
#./zephyr-sdk-0.17.4/setup.sh -c -t arm-zephyr-eabi
80+
# Cache the Zephyr SDK release assets in the image. CI still runs
81+
# `west sdk install` in its workspace, but the local proxy fallback can serve
82+
# these files without downloading them on every job.
83+
local zephyr_sdk_version
84+
zephyr_sdk_version="$(python3 "${SCRIPT_DIR}/zephyr_sdk_release_proxy.py" --print-version)"
85+
local zephyr_sdk_cache_dir="${ZEPHYR_SDK_RELEASE_PROXY_CACHE_DIR:-/opt/zephyr-sdk-cache/v${zephyr_sdk_version}}"
86+
python3 "${SCRIPT_DIR}/zephyr_sdk_release_proxy.py" \
87+
--version "${zephyr_sdk_version}" \
88+
--cache-dir "${zephyr_sdk_cache_dir}" \
89+
--populate-cache
9390
}
9491

95-
install_prerequiresites
92+
install_prerequisites
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Arm Limited and/or its affiliates.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Local Zephyr SDK release proxy for `west sdk install`.
8+
9+
This avoids GitHub releases API rate limits by serving a tiny synthetic releases
10+
API locally. Asset requests are served from a cache directory and populated with
11+
`wget` on cache miss.
12+
13+
Examples:
14+
15+
# Prepopulate CI cache only.
16+
.ci/docker/common/zephyr_sdk_release_proxy.py \
17+
--version <version> \
18+
--cache-dir .cache/zephyr-sdk/v<version> \
19+
--populate-cache
20+
21+
# Serve cached assets and release metadata.
22+
.ci/docker/common/zephyr_sdk_release_proxy.py \
23+
--version <version> \
24+
--cache-dir .cache/zephyr-sdk/v<version> \
25+
--port 8765
26+
27+
west sdk install \
28+
--version <version> \
29+
--api-url http://127.0.0.1:8765/releases \
30+
--gnu-toolchains arm-zephyr-eabi
31+
"""
32+
33+
from __future__ import annotations
34+
35+
import argparse
36+
import hashlib
37+
import http.server
38+
import json
39+
import os
40+
import platform
41+
import re
42+
import subprocess
43+
import sys
44+
import urllib.parse
45+
from pathlib import Path
46+
47+
48+
DEFAULT_SDK_VERSION = "1.0.1"
49+
50+
51+
class AssetVerificationError(Exception):
52+
pass
53+
54+
55+
def default_sdk_version() -> str:
56+
return (
57+
os.environ.get("ZEPHYR_SDK_VERSION")
58+
or os.environ.get("SDK_VERSION")
59+
or DEFAULT_SDK_VERSION
60+
)
61+
62+
63+
def host_tuple() -> tuple[str, str]:
64+
system = platform.system()
65+
machine = platform.machine()
66+
67+
if system != "Linux":
68+
raise SystemExit(f"Unsupported host OS: {system}")
69+
70+
if machine in ("x86_64", "AMD64"):
71+
return "linux", "x86_64"
72+
if machine in ("aarch64", "arm64"):
73+
return "linux", "aarch64"
74+
75+
raise SystemExit(f"Unsupported host architecture: {machine}")
76+
77+
78+
def release_base_url(version: str) -> str:
79+
return f"https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{version}"
80+
81+
82+
def asset_names(version: str, toolchain: str) -> list[str]:
83+
host_os, host_arch = host_tuple()
84+
return [
85+
"sha256.sum",
86+
f"zephyr-sdk-{version}_{host_os}-{host_arch}_minimal.tar.xz",
87+
f"hosttools_{host_os}-{host_arch}.tar.xz",
88+
f"toolchain_gnu_{host_os}-{host_arch}_{toolchain}.tar.xz",
89+
]
90+
91+
92+
def run_wget(url: str, output: Path) -> None:
93+
output.parent.mkdir(parents=True, exist_ok=True)
94+
partial = output.with_suffix(output.suffix + ".tmp")
95+
cmd = [
96+
"wget",
97+
"--continue",
98+
"--progress=bar:force:noscroll",
99+
"--output-document",
100+
str(partial),
101+
url,
102+
]
103+
subprocess.run(cmd, check=True)
104+
partial.replace(output)
105+
106+
107+
def ensure_asset(cache_dir: Path, version: str, filename: str) -> Path:
108+
path = cache_dir / filename
109+
if path.exists():
110+
return path
111+
112+
url = f"{release_base_url(version)}/{filename}"
113+
print(f"Downloading {url}", flush=True)
114+
run_wget(url, path)
115+
return path
116+
117+
118+
def parse_sha256_sum(path: Path) -> dict[str, str]:
119+
checksums: dict[str, str] = {}
120+
for line in path.read_text().splitlines():
121+
match = re.match(r"^([0-9a-fA-F]{64})\s+(.+)$", line.strip())
122+
if match:
123+
checksums[match.group(2)] = match.group(1).lower()
124+
return checksums
125+
126+
127+
def verify_asset(cache_dir: Path, checksums: dict[str, str], filename: str) -> None:
128+
if filename == "sha256.sum":
129+
return
130+
131+
expected = checksums.get(filename)
132+
if expected is None:
133+
raise AssetVerificationError(f"No checksum entry for {filename}")
134+
135+
hasher = hashlib.sha256()
136+
with (cache_dir / filename).open("rb") as f:
137+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
138+
hasher.update(chunk)
139+
digest = hasher.hexdigest()
140+
141+
if digest != expected:
142+
raise AssetVerificationError(
143+
f"Checksum mismatch for {filename}: expected {expected}, got {digest}"
144+
)
145+
146+
147+
def ensure_verified_asset(
148+
cache_dir: Path, version: str, checksums: dict[str, str], filename: str
149+
) -> Path:
150+
path = ensure_asset(cache_dir, version, filename)
151+
try:
152+
verify_asset(cache_dir, checksums, filename)
153+
except AssetVerificationError:
154+
path.unlink(missing_ok=True)
155+
path = ensure_asset(cache_dir, version, filename)
156+
verify_asset(cache_dir, checksums, filename)
157+
return path
158+
159+
160+
def populate_cache(cache_dir: Path, version: str, toolchain: str) -> None:
161+
cache_dir.mkdir(parents=True, exist_ok=True)
162+
names = asset_names(version, toolchain)
163+
164+
sha_file = ensure_asset(cache_dir, version, "sha256.sum")
165+
checksums = parse_sha256_sum(sha_file)
166+
167+
for name in names:
168+
ensure_verified_asset(cache_dir, version, checksums, name)
169+
170+
171+
def release_json(version: str, toolchain: str, base_url: str) -> bytes:
172+
assets = [
173+
{
174+
"name": name,
175+
"browser_download_url": f"{base_url}/assets/{name}",
176+
}
177+
for name in asset_names(version, toolchain)
178+
]
179+
return json.dumps([{"tag_name": f"v{version}", "assets": assets}]).encode()
180+
181+
182+
class SdkProxyHandler(http.server.SimpleHTTPRequestHandler):
183+
version: str
184+
toolchain: str
185+
cache_dir: Path
186+
187+
def log_message(self, fmt: str, *args: object) -> None:
188+
print(f"{self.address_string()} - {fmt % args}", file=sys.stderr)
189+
190+
def send_bytes(self, data: bytes, content_type: str) -> None:
191+
self.send_response(200)
192+
self.send_header("Content-Type", content_type)
193+
self.send_header("Content-Length", str(len(data)))
194+
self.end_headers()
195+
self.wfile.write(data)
196+
197+
def do_GET(self) -> None:
198+
parsed = urllib.parse.urlparse(self.path)
199+
200+
if parsed.path == "/releases":
201+
query = urllib.parse.parse_qs(parsed.query)
202+
page = query.get("page", ["1"])[0]
203+
if page == "1":
204+
host = self.headers.get("Host", "127.0.0.1")
205+
data = release_json(self.version, self.toolchain, f"http://{host}")
206+
else:
207+
data = b"[]"
208+
self.send_bytes(data, "application/json")
209+
return
210+
211+
if parsed.path.startswith("/assets/"):
212+
filename = Path(urllib.parse.unquote(parsed.path[len("/assets/") :])).name
213+
allowed = set(asset_names(self.version, self.toolchain))
214+
if filename not in allowed:
215+
self.send_error(404, f"Unknown asset: {filename}")
216+
return
217+
218+
try:
219+
if filename != "sha256.sum":
220+
sha_file = ensure_asset(self.cache_dir, self.version, "sha256.sum")
221+
path = ensure_verified_asset(
222+
self.cache_dir,
223+
self.version,
224+
parse_sha256_sum(sha_file),
225+
filename,
226+
)
227+
else:
228+
path = ensure_asset(self.cache_dir, self.version, filename)
229+
except (
230+
AssetVerificationError,
231+
subprocess.CalledProcessError,
232+
OSError,
233+
) as error:
234+
self.send_error(500, str(error))
235+
return
236+
237+
self.send_response(200)
238+
self.send_header("Content-Type", "application/octet-stream")
239+
self.send_header("Content-Length", str(path.stat().st_size))
240+
self.end_headers()
241+
with path.open("rb") as f:
242+
while chunk := f.read(1024 * 1024):
243+
self.wfile.write(chunk)
244+
return
245+
246+
self.send_error(404, "Not found")
247+
248+
249+
def default_cache_dir(version: str) -> Path:
250+
root = os.environ.get("XDG_CACHE_HOME")
251+
if root:
252+
return Path(root) / "zephyr-sdk" / f"v{version}"
253+
return Path.home() / ".cache" / "zephyr-sdk" / f"v{version}"
254+
255+
256+
def parse_args() -> argparse.Namespace:
257+
parser = argparse.ArgumentParser(description=__doc__)
258+
parser.add_argument("--version", default=default_sdk_version())
259+
parser.add_argument("--toolchain", default="arm-zephyr-eabi")
260+
parser.add_argument("--cache-dir", type=Path)
261+
parser.add_argument("--host", default="127.0.0.1")
262+
parser.add_argument("--port", type=int, default=8765)
263+
parser.add_argument(
264+
"--print-version",
265+
action="store_true",
266+
help="Print the effective default SDK version and exit.",
267+
)
268+
parser.add_argument(
269+
"--download-only",
270+
"--populate-cache",
271+
dest="download_only",
272+
action="store_true",
273+
help="Download and verify release assets, then exit without serving.",
274+
)
275+
return parser.parse_args()
276+
277+
278+
def main() -> None:
279+
args = parse_args()
280+
281+
if args.print_version:
282+
print(args.version)
283+
return
284+
285+
cache_dir = args.cache_dir or default_cache_dir(args.version)
286+
287+
if args.download_only:
288+
try:
289+
populate_cache(cache_dir, args.version, args.toolchain)
290+
except AssetVerificationError as error:
291+
raise SystemExit(str(error)) from error
292+
print(f"Cached Zephyr SDK assets in {cache_dir}")
293+
return
294+
295+
SdkProxyHandler.version = args.version
296+
SdkProxyHandler.toolchain = args.toolchain
297+
SdkProxyHandler.cache_dir = cache_dir
298+
299+
server = http.server.ThreadingHTTPServer((args.host, args.port), SdkProxyHandler)
300+
print(
301+
f"Serving Zephyr SDK release metadata at http://{args.host}:{args.port}/releases",
302+
flush=True,
303+
)
304+
print(f"Cache directory: {cache_dir}", flush=True)
305+
server.serve_forever()
306+
307+
308+
if __name__ == "__main__":
309+
main()

0 commit comments

Comments
 (0)