blob: 00ff7496430c9032b54779102eb8774134f1cef6 [file] [log] [blame] [edit]
#!/usr/bin/env python3
"""
Download and setup SLT (Silicon Labs Tools) CLI for Matter development.
This script downloads and installs the SLT CLI tool which is used to manage
Silicon Labs development tools required for Matter development.
"""
import argparse
import logging
import os
import re
import shutil
import stat
import subprocess
import sys
from zipfile import ZipFile
logger = logging.getLogger(__name__)
try:
import dload
except ImportError:
logger.error("dload package is required. Install it with: pip install dload")
sys.exit(1)
def setup_logging(verbose=False):
"""Configure logging level and format based on verbosity setting."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(level=level, format='[%(levelname)s] %(message)s')
def get_platform_vars():
"""Set platform-specific variables and URLs for SLT CLI download. Linux and macOS only."""
platform = sys.platform
if platform == "darwin":
platform_name = "mac"
elif platform == "linux":
platform_name = "linux"
else:
logger.error("Platform %s is not supported (Linux and macOS only)", platform)
sys.exit(1)
slt_cli_url = f"https://www.silabs.com/documents/public/software/slt-cli-1.0.1-{platform_name}-x64.zip"
return platform_name, slt_cli_url
def get_tools_path():
"""Return the script directory as the download location."""
script_dir = os.path.dirname(os.path.abspath(__file__))
os.makedirs(script_dir, exist_ok=True)
return script_dir
def get_install_done_marker_path():
"""Return path to marker file indicating install-packages has completed successfully."""
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, ".install-packages-done")
def get_files_slt_dir():
"""Return path to chip-build-efr32 files-slt directory."""
repo_root = get_repo_root()
return os.path.join(
repo_root, "integrations", "docker", "images", "stage-2", "chip-build-efr32", "files-slt"
)
def parse_version_from_slt(file_path):
"""Extract dependency version from a .slt file (version = \"X\" in [dependency] block). Returns None if missing."""
if not file_path or not os.path.isfile(file_path):
return None
# This regex finds the [dependency] section and captures the first version string in it.
pattern = re.compile(r"\[dependency\][^\[]*version\s*=\s*\"([^\"]+)\"")
try:
with open(file_path, "r") as f:
content = f.read()
except OSError:
return None
match = pattern.search(content)
if match:
version_str = match.group(1)
if "." in version_str:
version = version_str.split("@")[0].strip()
# TODO: Remove this override once a GA SiSDK release is available.
if version == "2025.12.1-alpha":
version = "2025.12.0"
return version
return None
def version_tuple(version_str):
"""Convert version string to tuple of integers for comparison (e.g. 2025.12.1-alpha -> (2025, 12, 1))."""
if not version_str:
return ()
main = version_str.split("-")[0].split("+")[0]
parts = []
for p in main.split("."):
try:
parts.append(int(p))
except ValueError:
parts.append(0)
return tuple(parts)
def version_greater(a, b):
"""Return True if version string a is greater than b."""
ta = version_tuple(a)
tb = version_tuple(b)
return ta > tb
def read_install_done_versions():
"""Read simplicity_sdk_version and wiseconnect_version from .install-packages-done. Returns None if missing."""
path = get_install_done_marker_path()
if not os.path.isfile(path):
return None
versions = {}
try:
with open(path, "r") as f:
for line in f:
line = line.strip()
if "=" in line:
key, _, value = line.partition("=")
versions[key.strip()] = value.strip()
except OSError:
return None
simplicity = versions.get("simplicity_sdk_version")
wiseconnect = versions.get("wiseconnect_version")
if simplicity is None or wiseconnect is None:
return None
return {"simplicity_sdk_version": simplicity, "wiseconnect_version": wiseconnect}
def check_skip_if_up_to_date():
"""
If .install-packages-done exists, compare its versions to sisdk-pkg.slt and wiseconnect-pkg.slt.
If both .slt requested versions are not greater than installed, exit 0 (skip execution).
"""
marker_path = get_install_done_marker_path()
if not os.path.isfile(marker_path):
return
done = read_install_done_versions()
if done is None:
return
files_slt_dir = get_files_slt_dir()
sisdk_slt = os.path.join(files_slt_dir, "sisdk-pkg.slt")
wiseconnect_slt = os.path.join(files_slt_dir, "wiseconnect-pkg.slt")
slt_simplicity = parse_version_from_slt(sisdk_slt)
slt_wiseconnect = parse_version_from_slt(wiseconnect_slt)
if slt_simplicity is None:
logger.error("Could not parse version from %s", sisdk_slt)
sys.exit(1)
if slt_wiseconnect is None:
logger.error("Could not parse version from %s", wiseconnect_slt)
sys.exit(1)
need_simplicity = version_greater(slt_simplicity, done["simplicity_sdk_version"])
need_wiseconnect = version_greater(slt_wiseconnect, done["wiseconnect_version"])
if not need_simplicity and not need_wiseconnect:
logger.info(
"Installed SDK versions (%s, %s) are already >= requested in .slt (%s, %s). Skipping.",
done["simplicity_sdk_version"],
done["wiseconnect_version"],
slt_simplicity,
slt_wiseconnect,
)
sys.exit(0)
def make_executable(path):
"""Make a file executable on Unix-like systems."""
if path and os.path.exists(path):
try:
st = os.stat(path)
os.chmod(path, st.st_mode | stat.S_IEXEC)
except OSError as e:
logger.warning("Could not make %s executable: %s", path, e)
else:
logger.warning("Path %s does not exist to make executable.", path)
def download_slt_cli():
"""Download and extract SLT CLI tool to script directory."""
platform_name, slt_cli_url = get_platform_vars()
tools_folder_path = get_tools_path()
slt_cli_path = os.path.join(tools_folder_path, "slt")
if os.path.isfile(slt_cli_path):
logger.info("SLT CLI already exists at %s", slt_cli_path)
return slt_cli_path
logger.info("Downloading and unzipping slt-cli...")
slt_zip_path = os.path.join(tools_folder_path, "slt.zip")
try:
dload.save(slt_cli_url, slt_zip_path)
with ZipFile(slt_zip_path, 'r') as zObject:
# Check for path traversal vulnerabilities before extracting
for member in zObject.infolist():
resolved_path = os.path.realpath(os.path.join(tools_folder_path, member.filename))
if not resolved_path.startswith(os.path.realpath(tools_folder_path)):
logger.error("Zip file contains unsafe path: %s", member.filename)
sys.exit(1)
# If all paths are safe, extract
zObject.extractall(path=tools_folder_path)
os.remove(slt_zip_path)
make_executable(slt_cli_path)
logger.info("SLT CLI installed at %s", slt_cli_path)
return slt_cli_path
except Exception as e:
logger.error("Failed to download/extract slt-cli: %s", e)
sys.exit(1)
def update_slt_cli(slt_cli_path):
"""Update SLT CLI to latest version."""
update_cmd = [slt_cli_path, "update", "--self"]
try:
logger.info("Updating SLT CLI to latest version...")
subprocess.run(update_cmd, check=True)
logger.info("SLT CLI updated successfully")
except subprocess.CalledProcessError as e:
logger.warning("Failed to update slt-cli: %s", e)
except FileNotFoundError:
logger.warning("SLT CLI not found at %s, skipping update", slt_cli_path)
def get_pkg_manifest_paths():
"""Return paths to sisdk-pkg.lock and wiseconnect-pkg.slt from chip-build-efr32 files-slt."""
repo_root = get_repo_root()
files_slt_dir = os.path.join(
repo_root, "integrations", "docker", "images", "stage-2", "chip-build-efr32", "files-slt"
)
return [
os.path.join(files_slt_dir, "wiseconnect-pkg.slt"),
os.path.join(files_slt_dir, "sisdk-pkg.lock"),
]
def install_sdk_packages(slt_cli_path):
"""Install packages from sisdk-pkg.lock and wiseconnect-pkg.slt."""
for pkg_path in get_pkg_manifest_paths():
if not os.path.isfile(pkg_path):
logger.error("Package manifest not found at %s", pkg_path)
sys.exit(1)
for pkg_path in get_pkg_manifest_paths():
install_cmd = [slt_cli_path, "install", "-f", pkg_path]
try:
logger.info("Installing packages from %s...", os.path.basename(pkg_path))
subprocess.run(install_cmd, check=True)
logger.info("Packages from %s installed successfully", os.path.basename(pkg_path))
except subprocess.CalledProcessError as e:
logger.error("Failed to install packages from %s: %s", pkg_path, e)
sys.exit(1)
def slt_where(slt_cli_path, package):
"""Run 'slt where <package>' and return the path, or None if not found."""
try:
result = subprocess.run(
[slt_cli_path, "where", package],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except (subprocess.SubprocessError, FileNotFoundError):
pass
return None
def get_repo_root():
"""Return the repository root (three levels up from this script)."""
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
SILABS_SDK_LINK_NAMES = ("simplicity_sdk", "wifi_sdk")
def is_git_submodule_checkout(dir_path):
"""Return True if dir_path is a directory that is a git submodule checkout (e.g. from older installs)."""
if not dir_path or not os.path.isdir(dir_path) or os.path.islink(dir_path):
return False
git_file = os.path.join(dir_path, ".git")
if not os.path.isfile(git_file):
return False
try:
with open(git_file, "r") as f:
return f.read().strip().startswith("gitdir:")
except OSError:
return False
def get_silabs_paths_as_submodules(repo_root):
"""Return which of third_party/silabs/simplicity_sdk and wifi_sdk exist as submodule checkouts on disk."""
found = []
for name in SILABS_SDK_LINK_NAMES:
path = os.path.join(repo_root, "third_party", "silabs", name)
if is_git_submodule_checkout(path):
found.append(name)
return found
def _remove_silabs_submodules(repo_root, submodule_names):
"""Remove submodule checkout dirs: git rm when path is in index; else remove from disk (e.g. after pull, index already has no entry)."""
paths = [os.path.join("third_party", "silabs", n) for n in submodule_names]
for path in paths:
logger.info("Running: git rm %s", path)
r = subprocess.run(["git", "rm", path], cwd=repo_root, capture_output=True, text=True)
if r.returncode == 0:
continue
full_path = os.path.join(repo_root, path)
if os.path.isdir(full_path):
logger.warning("Path not known to Git, removing directory from disk: %s", path)
try:
shutil.rmtree(full_path)
except OSError as e:
logger.error("Failed to remove %s: %s", path, e)
sys.exit(1)
else:
logger.error("Failed to remove %s: %s", path, r.stderr.strip() or r.stdout.strip())
sys.exit(1)
logger.info("Submodules removed successfully.")
def check_silabs_not_submodules(repo_root):
"""If simplicity_sdk or wifi_sdk are submodule checkouts, ask to remove them; else exit with instructions."""
submodule_names = get_silabs_paths_as_submodules(repo_root)
if not submodule_names:
return
paths = [os.path.join("third_party", "silabs", n) for n in submodule_names]
logger.warning(
"The following paths are submodule checkouts (e.g. from older installs): %s. "
"This script expects to create symlinks here, not use submodules.",
", ".join(paths),
)
try:
reply = input("Remove these submodules now? [y/N]: ").strip().lower()
except EOFError:
reply = "n"
if reply in ("y", "yes"):
_remove_silabs_submodules(repo_root, submodule_names)
else:
logger.error(
"Remove the directories manually (e.g. rm -rf %s), then re-run this script.",
" ".join(paths),
)
sys.exit(1)
def create_sdk_symlinks(simplicity_sdk_path, wiseconnect_path):
"""Create symlinks: third_party/silabs/simplicity_sdk and wifi_sdk to SLT SDK locations."""
repo_root = get_repo_root()
silabs_dir = os.path.join(repo_root, "third_party", "silabs")
def create_symlink(target_path, link_name):
if not target_path or not os.path.isdir(target_path):
logger.warning("Target path does not exist or is not a directory: %s", target_path)
return
link_path = os.path.join(silabs_dir, link_name)
try:
os.makedirs(silabs_dir, exist_ok=True)
if os.path.lexists(link_path):
if os.path.islink(link_path):
current = os.path.realpath(link_path)
if os.path.realpath(target_path) == current:
logger.info("Symlink already up to date: %s", link_path)
return
os.remove(link_path)
else:
logger.warning("Path exists and is not a symlink, skipping: %s", link_path)
return
os.symlink(target_path, link_path)
logger.info("Created symlink %s -> %s", link_path, target_path)
except OSError as e:
logger.warning("Could not create symlink %s: %s", link_path, e)
create_symlink(simplicity_sdk_path, "simplicity_sdk")
create_symlink(wiseconnect_path, "wifi_sdk")
def parse_key_from_file(file_path, key):
"""Return the value for the first line 'key: value' in file_path, or None if missing."""
if not file_path or not os.path.isfile(file_path):
return None
prefix = key + ": "
try:
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if line.startswith(prefix):
return line[len(prefix):].strip()
except OSError as e:
logger.warning("Could not read file %s: %s", file_path, e)
return None
def get_installed_sdk_versions(repo_root):
"""Read sdk_version from simplicity_sdk.slcs and version from wiseconnect.slce under third_party/silabs.
Exits with an error if either file or key is missing.
"""
silabs = os.path.join(repo_root, "third_party", "silabs")
simplicity_slcs = os.path.join(silabs, "simplicity_sdk", "simplicity_sdk.slcs")
wiseconnect_slce = os.path.join(silabs, "wifi_sdk", "wiseconnect.slce")
simplicity_sdk_version = parse_key_from_file(simplicity_slcs, "sdk_version")
wiseconnect_version = parse_key_from_file(wiseconnect_slce, "version")
missing = []
if simplicity_sdk_version is None:
missing.append("sdk_version from %s" % simplicity_slcs)
if wiseconnect_version is None:
missing.append("version from %s" % wiseconnect_slce)
if missing:
logger.error("Could not read required version fields: %s", "; ".join(missing))
sys.exit(1)
return {
"simplicity_sdk_version": simplicity_sdk_version,
"wiseconnect_version": wiseconnect_version,
}
def write_install_done_marker(versions):
"""Write .install-packages-done with installed SDK versions."""
marker_path = get_install_done_marker_path()
lines = [f"{key}={value}" for key, value in versions.items()]
try:
with open(marker_path, "w") as f:
f.write("\n".join(lines))
f.write("\n")
except OSError as e:
logger.error("Could not write install-done marker: %s", e)
sys.exit(1)
def setup_slt_environment(verbose=False):
"""Main function to setup SLT CLI and install required packages."""
setup_logging(verbose)
check_skip_if_up_to_date()
slt_cli_path = download_slt_cli()
update_slt_cli(slt_cli_path)
install_sdk_packages(slt_cli_path)
repo_root = get_repo_root()
check_silabs_not_submodules(repo_root)
simplicity_sdk_path = slt_where(slt_cli_path, "simplicity-sdk/2025.12.1-alpha")
wiseconnect_path = slt_where(slt_cli_path, "wiseconnect")
create_sdk_symlinks(simplicity_sdk_path, wiseconnect_path)
versions = get_installed_sdk_versions(repo_root)
write_install_done_marker(versions)
return slt_cli_path
def main():
parser = argparse.ArgumentParser(
description="Download and setup SLT (Silicon Labs Tools) CLI for Matter development."
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose (debug) logging'
)
args = parser.parse_args()
slt_path = setup_slt_environment(verbose=args.verbose)
logger.info("\nSLT CLI setup completed successfully")
logger.info("SLT CLI location: %s", slt_path)
if __name__ == "__main__":
main()