575 lines
20 KiB
Python
Executable File
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()
|