zephyr/scripts/west_commands/sdk.py

575 lines
20 KiB
Python
Executable File

# Copyright (c) 2024 TOKITA Hiroshi
#
# SPDX-License-Identifier: Apache-2.0
import argparse
import hashlib
import os
import patoolib
import platform
import re
import requests
import semver
import shutil
import subprocess
import tempfile
import textwrap
import zcmake
from pathlib import Path
from west.commands import WestCommand
class Sdk(WestCommand):
def __init__(self):
super().__init__(
"sdk",
"manage Zephyr SDK",
"List and Install Zephyr SDK",
)
def do_add_parser(self, parser_adder):
parser = parser_adder.add_parser(
self.name,
help=self.help,
description=self.description,
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
"""
Listing SDKs:
Run 'west sdk' or 'west sdk list' to list installed SDKs.
See 'west sdk list --help' for details.
Installing SDK:
Run 'west sdk install' to install Zephyr SDK.
See 'west sdk install --help' for details.
"""
),
)
subparsers_gen = parser.add_subparsers(
metavar="<subcommand>",
dest="subcommand",
help="select a subcommand. If omitted, treat it as the 'list' selected.",
)
subparsers_gen.add_parser(
"list",
help="list installed Zephyr SDKs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
"""
Listing SDKs:
Run 'west sdk' or 'west sdk list' command information about available SDKs is displayed.
"""
),
)
install_args_parser = subparsers_gen.add_parser(
"install",
help="install Zephyr SDK",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
"""
Installing SDK:
Run 'west sdk install' to install Zephyr SDK.
Set --version option to install a specific version of the SDK.
If not specified, the install version is detected from "${ZEPHYR_BASE}/SDK_VERSION file.
SDKs older than 0.14.1 are not supported.
You can specify the installation directory with --install-dir or --install-base.
If the specified version of the SDK is already installed,
the already installed SDK will be used regardless of the settings of
--install-dir and --install-base.
Typically, Zephyr SDK archives contain only one directory named zephyr-sdk-<version>
at the top level.
The SDK archive is extracted to the home directory if both --install-dir and --install-base
are not specified.
In this case, SDK will install into ${HOME}/zephyr-sdk-<version>.
If --install-base is specified, the archive will be extracted under the specified path.
In this case, SDK will install into <BASE>/zephyr-sdk-<version> .
If --install-dir is specified, the directory contained in the archive will be renamed
and placed to the specified location.
--interactive, --toolchains, --no-toolchains and --no-hosttools options
specify the behavior of the installer. Please see the description of each option.
--personal-access-token specifies the GitHub personal access token.
This helps to relax the limits on the number of REST API calls.
--api-url specifies the REST API endpoint for GitHub releases information
when installing the SDK from a different GitHub repository.
"""
),
)
install_args_parser.add_argument(
"--version",
default=None,
nargs="?",
metavar="SDK_VER",
help="version of the Zephyr SDK to install. "
"If not specified, the install version is detected from "
"${ZEPHYR_BASE}/SDK_VERSION file.",
)
install_args_parser.add_argument(
"-b",
"--install-base",
default=None,
metavar="BASE",
help="Base directory to SDK install. "
"The subdirectory created by extracting the archive in <BASE> will be the SDK installation directory. "
"For example, -b /foo/bar will install the SDK in `/foo/bar/zephyr-sdk-<version>'."
)
install_args_parser.add_argument(
"-d",
"--install-dir",
default=None,
metavar="DIR",
help="SDK install destination directory. "
"The SDK will be installed on the specified path. "
"The directory contained in the archive will be renamed and installed for the specified directory. "
"For example, if you specify -b /foo/bar/baz, The archive's zephyr-sdk-<version> directory will be renamed baz and placed under /foo/bar. "
"If this option is specified, the --install-base option is ignored. "
)
install_args_parser.add_argument(
"-i",
"--interactive",
action="store_true",
help="launches installer in interactive mode. "
"--toolchains, --no-toolchains and --no-hosttools will be ignored if this option is enabled.",
)
install_args_parser.add_argument(
"-t",
"--toolchains",
metavar="toolchain_name",
nargs="+",
help="toolchain(s) to install (e.g. 'arm-zephyr-eabi'). "
"If this option is not given, toolchains for all architectures will be installed.",
)
install_args_parser.add_argument(
"-T",
"--no-toolchains",
action="store_true",
help="do not install toolchains. "
"--toolchains will be ignored if this option is enabled.",
)
install_args_parser.add_argument(
"-H",
"--no-hosttools",
action="store_true",
help="do not install host-tools.",
)
install_args_parser.add_argument(
"--personal-access-token", help="GitHub personal access token."
)
install_args_parser.add_argument(
"--api-url",
default="https://api.github.com/repos/zephyrproject-rtos/sdk-ng/releases",
help="GitHub releases API endpoint used to look for Zephyr SDKs.",
)
return parser
def os_arch_name(self):
system = platform.system()
machine = platform.machine()
if system == "Linux":
osname = "linux"
elif system == "Darwin":
osname = "macos"
elif system == "Windows":
osname = "windows"
else:
self.die(f"Unsupported system: {system}")
if machine in ["aarch64", "arm64"]:
arch = "aarch64"
elif machine in ["x86_64", "AMD64"]:
arch = "x86_64"
else:
self.die(f"Unsupported machine: {machine}")
return (osname, arch)
def detect_version(self, args):
if args.version:
version = args.version
else:
if os.environ["ZEPHYR_BASE"]:
zephyr_base = Path(os.environ["ZEPHYR_BASE"])
else:
zephyr_base = Path(__file__).parents[2]
sdk_version_file = zephyr_base / "SDK_VERSION"
if not sdk_version_file.exists():
self.die(f"{str(sdk_version_file)} does not exist.")
with open(sdk_version_file) as f:
version = f.readlines()[0].strip()
self.inf(
f"Found '{str(sdk_version_file)}', installing version {version}."
)
try:
semver.Version.parse(version)
except Exception:
self.die(f"Invalid version format: {version}")
if semver.compare(version, "0.14.1") < 0:
self.die(f"Versions older than v0.14.1 are not supported.")
return version
def fetch_releases(self, url, req_headers):
"""fetch releases data via GitHub REST API"""
releases = []
page = 1
while True:
params = {"page": page, "per_page": 100}
resp = requests.get(url, headers=req_headers, params=params)
if resp.status_code != 200:
raise Exception(f"Failed to fetch: {resp.status_code}, {resp.text}")
data = resp.json()
if not data:
break
releases.extend(data)
page += 1
return releases
def minimal_sdk_filename(self, release):
(osname, arch) = self.os_arch_name()
version = re.sub(r"^v", "", release["tag_name"])
if semver.compare(version, "0.16.0") < 0:
if osname == "windows":
ext = ".zip"
else:
ext = ".tar.gz"
else:
if osname == "windows":
ext = ".7z"
else:
ext = ".tar.xz"
return f"zephyr-sdk-{version}_{osname}-{arch}_minimal{ext}"
def minimal_sdk_sha256(self, sha256_list, release):
name = self.minimal_sdk_filename(release)
tuples = [(re.split(r"\s+", t)) for t in sha256_list.splitlines()]
hashtable = {t[1]: t[0] for t in tuples}
return hashtable[name]
def minimal_sdk_url(self, release):
name = self.minimal_sdk_filename(release)
assets = release.get("assets", [])
minimal_sdk_asset = next(filter(lambda x: x["name"] == name, assets))
return minimal_sdk_asset["browser_download_url"]
def sha256_sum_url(self, release):
assets = release.get("assets", [])
minimal_sdk_asset = next(filter(lambda x: x["name"] == "sha256.sum", assets))
return minimal_sdk_asset["browser_download_url"]
def download_and_extract(self, base_dir, dir_name, target_release, req_headers):
self.inf("Fetching sha256...")
sha256_url = self.sha256_sum_url(target_release)
resp = requests.get(sha256_url, headers=req_headers, stream=True)
if resp.status_code != 200:
raise Exception(f"Failed to download {sha256_url}: {resp.status_code}")
sha256 = self.minimal_sdk_sha256(resp.content.decode("UTF-8"), target_release)
archive_url = self.minimal_sdk_url(target_release)
self.inf(f"Downloading {archive_url}...")
resp = requests.get(archive_url, headers=req_headers, stream=True)
if resp.status_code != 200:
raise Exception(f"Failed to download {archive_url}: {resp.status_code}")
try:
Path(base_dir).mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=base_dir) as tempdir:
# download archive file
filename = Path(tempdir) / re.sub(r"^.*/", "", archive_url)
file = open(filename, mode="wb")
total_length = int(resp.headers["Content-Length"])
count = 0
for chunk in resp.iter_content(chunk_size=8192):
file.write(chunk)
count = count + len(chunk)
self.inf(f"\r {count}/{total_length}", end="")
self.inf()
self.inf(f"Downloaded: {file.name}")
file.close()
# check sha256 hash
with open(file.name, "rb") as sha256file:
digest = hashlib.sha256(sha256file.read()).hexdigest()
if sha256 != digest:
raise Exception(f"sha256 mismatched: {sha256}:{digest}")
# extract archive file
self.inf(f"Extract: {file.name}")
patoolib.extract_archive(file.name, outdir=tempdir)
# We expect that only the zephyr-sdk-x.y.z directory will be present in the archive.
extracted_dirs = [d for d in Path(tempdir).iterdir() if d.is_dir()]
if len(extracted_dirs) != 1:
raise Exception("Unexpected archive format")
# move to destination dir
if dir_name:
dest_dir = Path(base_dir / dir_name)
else:
dest_dir = Path(base_dir / extracted_dirs[0].name)
Path(dest_dir).parent.mkdir(parents=True, exist_ok=True)
self.inf(f"Move: {str(extracted_dirs[0])} to {dest_dir}.")
shutil.move(extracted_dirs[0], dest_dir)
return dest_dir
except PermissionError as pe:
self.die(pe)
def run_setup(self, args, sdk_dir):
if "Windows" == platform.system():
setup = Path(sdk_dir) / "setup.cmd"
optsep = "/"
else:
setup = Path(sdk_dir) / "setup.sh"
optsep = "-"
# Associate installed SDK so that it can be found.
cmds_cmake_pkg = [str(setup), f"{optsep}c"]
self.dbg("Run: ", cmds_cmake_pkg)
result = subprocess.run(cmds_cmake_pkg)
if result.returncode != 0:
self.die(f"command \"{' '.join(cmds_cmake_pkg)}\" failed")
cmds = [str(setup)]
if not args.interactive and not args.no_toolchains:
if not args.toolchains:
cmds.extend([f"{optsep}t", "all"])
else:
for tc in args.toolchains:
cmds.extend([f"{optsep}t", tc])
if not args.interactive and not args.no_hosttools:
cmds.extend([f"{optsep}h"])
if args.interactive or len(cmds) != 1:
self.dbg("Run: ", cmds)
result = subprocess.run(cmds)
if result.returncode != 0:
self.die(f"command \"{' '.join(cmds)}\" failed")
def install_sdk(self, args, user_args):
version = self.detect_version(args)
(osname, arch) = self.os_arch_name()
if args.personal_access_token:
req_headers = {
"Authorization": f"Bearer {args.personal_access_token}",
}
else:
req_headers = {}
self.inf("Fetching Zephyr SDK list...")
releases = self.fetch_releases(args.api_url, req_headers)
self.dbg("releases: ", "\n".join([x["tag_name"] for x in releases]))
# checking version
def check_semver(version):
try:
semver.Version.parse(version)
return True
except Exception:
return False
available_versions = [
re.sub(r"^v", "", x["tag_name"])
for x in releases
if check_semver(re.sub(r"^v", "", x["tag_name"]))
]
if not version in available_versions:
self.die(
f"Unavailable SDK version: {version}."
+ "Please select from the list below:\n"
+ "\n".join(available_versions)
)
target_release = [x for x in releases if x["tag_name"] == f"v{version}"][0]
# checking toolchains parameters
assets = target_release["assets"]
self.dbg("assets: ", "\n".join([x["browser_download_url"] for x in assets]))
prefix = f"toolchain_{osname}-{arch}_"
available_toolchains = [
re.sub(r"\..*", "", x["name"].replace(prefix, ""))
for x in assets
if x["name"].startswith(prefix)
]
if args.toolchains:
for tc in args.toolchains:
if not tc in available_toolchains:
self.die(
f"toolchain {tc} is not available.\n"
+ "Please select from the list below:\n"
+ "\n".join(available_toolchains)
)
installed_info = [v for (k, v) in self.fetch_sdk_info().items() if k == version]
if len(installed_info) == 0:
if args.install_dir:
base_dir = Path(args.install_dir).parent
dir_name = Path(args.install_dir).name
elif args.install_base:
base_dir = Path(args.install_base)
dir_name = None
else:
base_dir = Path("~").expanduser()
dir_name = None
sdk_dir = self.download_and_extract(
base_dir, dir_name, target_release, req_headers
)
else:
sdk_dir = Path(installed_info[0]["path"])
self.inf(
f"Zephyr SDK version {version} is already installed at {str(sdk_dir)}. Using it."
)
self.run_setup(args, sdk_dir)
def fetch_sdk_info(self):
sdk_lines = []
try:
cmds = [
"-P",
str(Path(__file__).parent / "sdk" / "listsdk.cmake"),
]
output = zcmake.run_cmake(cmds, capture_output=True)
if output:
# remove '-- Zephyr-sdk,' leader
sdk_lines = [l[15:] for l in output if l.startswith("-- Zephyr-sdk,")]
else:
sdk_lines = []
except Exception as e:
self.die(e)
def parse_sdk_entry(line):
class SdkEntry:
def __init__(self):
self.version = None
self.path = None
info = SdkEntry()
for ent in line.split(","):
kv = ent.split("=")
if kv[0].strip() == "ver":
info.version = kv[1].strip()
elif kv[0].strip() == "dir":
info.path = kv[1].strip()
return info
sdk_info = {}
for sdk_ent in [parse_sdk_entry(l) for l in reversed(sdk_lines)]:
entry = {}
ver = None
sdk_path = Path(sdk_ent.path)
sdk_version_path = sdk_path / "sdk_version"
if sdk_version_path.exists():
with open(str(sdk_version_path)) as f:
ver = f.readline().strip()
else:
continue
entry["path"] = sdk_path
if (sdk_path / "sysroots").exists():
entry["hosttools"] = "installed"
# Identify toolchain directory by the existence of <toolchain>/bin/<toolchain>-gcc
if "Windows" == platform.system():
gcc_postfix = "-gcc.exe"
else:
gcc_postfix = "-gcc"
toolchains = [
tc.name
for tc in sdk_path.iterdir()
if (sdk_path / tc / "bin" / (tc.name + gcc_postfix)).exists()
]
if len(toolchains) > 0:
entry["toolchains"] = toolchains
if ver:
sdk_info[ver] = entry
return sdk_info
def list_sdk(self):
sdk_info = self.fetch_sdk_info()
if len(sdk_info) == 0:
self.die("No Zephyr SDK installed.")
for k, v in sdk_info.items():
self.inf(f"{k}:")
self.inf(f" path: {v['path']}")
if "hosttools" in v:
self.inf(f" hosttools: {v['hosttools']}")
if "toolchains" in v:
self.inf(" installed-toolchains:")
for tc in v["toolchains"]:
self.inf(f" - {tc}")
# Since version 0.15.2, the sdk_toolchains file is included,
# so we can get information about available toolchains from there.
if (Path(v["path"]) / "sdk_toolchains").exists():
with open(Path(v["path"]) / "sdk_toolchains") as f:
all_tcs = [l.strip() for l in f.readlines()]
self.inf(" available-toolchains:")
for tc in all_tcs:
if tc not in v["toolchains"]:
self.inf(f" - {tc}")
self.inf()
def do_run(self, args, user_args):
self.dbg("args: ", args)
if args.subcommand == "install":
self.install_sdk(args, user_args)
elif args.subcommand == "list" or not args.subcommand:
self.list_sdk()