Skip to content

Commit

Permalink
[platform] Install ltchiptool in separate virtual environment (#166)
Browse files Browse the repository at this point in the history
* [platform] Install ltchiptool in separate virtual environment

* [platform] Fix f-string syntax, set LibreTiny path in ltchiptool

* [platform] Fix venv site-packages path

* [platform] Fix installing pip without ensurepip

* [platform] Install binary dependencies only
  • Loading branch information
kuba2k2 authored Sep 10, 2023
1 parent 3750ae6 commit 0f5d0a8
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 78 deletions.
9 changes: 8 additions & 1 deletion builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
platform: PlatformBase = env.PioPlatform()
board: PlatformBoardConfig = env.BoardConfig()

python_deps = {
"ltchiptool": ">=4.5.1,<5.0",
}
env.SConscript("python-venv.py", exports="env")
env.ConfigurePythonVenv()
env.InstallPythonDependencies(python_deps)

# Utilities
env.SConscript("utils/config.py", exports="env")
env.SConscript("utils/cores.py", exports="env")
env.SConscript("utils/env.py", exports="env")
env.SConscript("utils/flash.py", exports="env")
env.SConscript("utils/libs-external.py", exports="env")
env.SConscript("utils/libs-queue.py", exports="env")
env.SConscript("utils/ltchiptool.py", exports="env")
env.SConscript("utils/ltchiptool-util.py", exports="env")

# Firmware name
if env.get("PROGNAME", "program") == "program":
Expand Down
122 changes: 122 additions & 0 deletions builder/python-venv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright (c) Kuba Szczodrzyński 2023-09-07.

import json
import site
import subprocess
import sys
from pathlib import Path

import semantic_version
from platformio.compat import IS_WINDOWS
from platformio.package.version import pepver_to_semver
from platformio.platform.base import PlatformBase
from SCons.Script import DefaultEnvironment, Environment

env: Environment = DefaultEnvironment()
platform: PlatformBase = env.PioPlatform()

# code borrowed and modified from espressif32/builder/frameworks/espidf.py


def env_configure_python_venv(env: Environment):
venv_path = Path(env.subst("${PROJECT_CORE_DIR}"), "penv", ".libretiny")

pip_path = venv_path.joinpath(
"Scripts" if IS_WINDOWS else "bin",
"pip" + (".exe" if IS_WINDOWS else ""),
)
python_path = venv_path.joinpath(
"Scripts" if IS_WINDOWS else "bin",
"python" + (".exe" if IS_WINDOWS else ""),
)
site_path = venv_path.joinpath(
"Lib" if IS_WINDOWS else "lib",
"." if IS_WINDOWS else f"python{sys.version_info[0]}.{sys.version_info[1]}",
"site-packages",
)

if not pip_path.is_file():
# Use the built-in PlatformIO Python to create a standalone virtual env
result = env.Execute(
env.VerboseAction(
f'"$PYTHONEXE" -m venv --clear "{venv_path.absolute()}"',
"LibreTiny: Creating a virtual environment for Python dependencies",
)
)
if not python_path.is_file():
# Creating the venv failed
raise RuntimeError(
f"Failed to create virtual environment. Error code {result}"
)
if not pip_path.is_file():
# Creating the venv succeeded but pip didn't get installed
# (i.e. Debian/Ubuntu without ensurepip)
print(
"LibreTiny: Failed to install pip, running get-pip.py", file=sys.stderr
)
import requests

with requests.get("https://bootstrap.pypa.io/get-pip.py") as r:
p = subprocess.Popen(
args=str(python_path.absolute()),
stdin=subprocess.PIPE,
)
p.communicate(r.content)
p.wait()

assert (
pip_path.is_file()
), f"Error: Missing the pip binary in virtual environment `{pip_path.absolute()}`"
assert (
python_path.is_file()
), f"Error: Missing Python executable file `{python_path.absolute()}`"
assert (
site_path.is_dir()
), f"Error: Missing site-packages directory `{site_path.absolute()}`"

env.Replace(LTPYTHONEXE=python_path.absolute(), LTPYTHONENV=venv_path.absolute())
site.addsitedir(str(site_path.absolute()))


def env_install_python_dependencies(env: Environment, dependencies: dict):
try:
pip_output = subprocess.check_output(
[
env.subst("${LTPYTHONEXE}"),
"-m",
"pip",
"list",
"--format=json",
"--disable-pip-version-check",
]
)
pip_data = json.loads(pip_output)
packages = {p["name"]: pepver_to_semver(p["version"]) for p in pip_data}
except:
print(
"LibreTiny: Warning! Couldn't extract the list of installed Python packages"
)
packages = {}

to_install = []
for name, spec in dependencies.items():
install_spec = f'"{name}{dependencies[name]}"'
if name not in packages:
to_install.append(install_spec)
elif spec:
version_spec = semantic_version.Spec(spec)
if not version_spec.match(packages[name]):
to_install.append(install_spec)

if to_install:
env.Execute(
env.VerboseAction(
'"${LTPYTHONEXE}" -m pip install --prefer-binary -U '
+ " ".join(to_install),
"LibreTiny: Installing Python dependencies",
)
)


env.AddMethod(env_configure_python_venv, "ConfigurePythonVenv")
env.AddMethod(env_install_python_dependencies, "InstallPythonDependencies")
5 changes: 4 additions & 1 deletion builder/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Dict

from ltchiptool import Family, get_version
from ltchiptool.util.lvm import LVM
from ltchiptool.util.misc import sizeof
from platformio.platform.base import PlatformBase
from platformio.platform.board import PlatformBoardConfig
Expand Down Expand Up @@ -77,7 +78,7 @@ def env_configure(
# ltchiptool config:
# -r output raw log messages
# -i 1 indent log messages
LTCHIPTOOL='"${PYTHONEXE}" -m ltchiptool -r -i 1',
LTCHIPTOOL='"${LTPYTHONEXE}" -m ltchiptool -r -i 1 -L "${LT_DIR}"',
# Fix for link2bin to get tmpfile name in argv
LINKCOM="${LINK} ${LINKARGS}",
LINKARGS="${TEMPFILE('-o $TARGET $LINKFLAGS $__RPATH $SOURCES $_LIBDIRFLAGS $_LIBFLAGS', '$LINKCOMSTR')}",
Expand All @@ -87,6 +88,8 @@ def env_configure(
)
# Store family parameters as environment variables
env.Replace(**dict(family))
# Set platform directory in ltchiptool (for use in this process only)
LVM.add_path(platform.get_dir())
return family


Expand Down
File renamed without changes.
90 changes: 14 additions & 76 deletions platform.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Copyright (c) Kuba Szczodrzyński 2022-04-20.

import importlib
import json
import os
import platform
import site
import sys
from os.path import dirname
from subprocess import Popen
from pathlib import Path
from typing import Dict, List

import click
Expand All @@ -15,73 +15,8 @@
from platformio.package.meta import PackageItem
from platformio.platform.base import PlatformBase
from platformio.platform.board import PlatformBoardConfig
from semantic_version import SimpleSpec, Version

LTCHIPTOOL_VERSION = "^4.2.3"


# Install & import tools
def check_ltchiptool(install: bool):
if install:
# update ltchiptool to a supported version
print("Installing/updating ltchiptool")
p = Popen(
[
sys.executable,
"-m",
"pip",
"install",
"-U",
"--force-reinstall",
f"ltchiptool >= {LTCHIPTOOL_VERSION[1:]}, < 5.0",
],
)
p.wait()

# unload all modules from the old version
for name, module in list(sorted(sys.modules.items())):
if not name.startswith("ltchiptool"):
continue
del sys.modules[name]
del module

# try to import it
ltchiptool = importlib.import_module("ltchiptool")

# check if the version is known
version = Version.coerce(ltchiptool.get_version()).truncate()
if version in SimpleSpec(LTCHIPTOOL_VERSION):
return
if not install:
raise ImportError(f"Version incompatible: {version}")


def try_check_ltchiptool():
install_modes = [False, True]
exception = None
for install in install_modes:
try:
check_ltchiptool(install)
return
except (ImportError, AttributeError) as ex:
exception = ex
print(
"!!! Installing ltchiptool failed, or version outdated. "
"Please install ltchiptool manually using pip. "
f"Cannot continue. {type(exception).name}: {exception}"
)
raise exception


try_check_ltchiptool()
import ltchiptool

# Remove current dir so it doesn't conflict with PIO
if dirname(__file__) in sys.path:
sys.path.remove(dirname(__file__))

# Let ltchiptool know about LT's location
ltchiptool.lt_set_path(dirname(__file__))
site.addsitedir(Path(__file__).absolute().parent.joinpath("tools"))


def get_os_specifiers():
Expand Down Expand Up @@ -119,6 +54,12 @@ def __init__(self, manifest_path):
super().__init__(manifest_path)
self.custom_opts = {}
self.versions = {}
self.verbose = (
"-v" in sys.argv
or "--verbose" in sys.argv
or "PIOVERBOSE=1" in sys.argv
or os.environ.get("PIOVERBOSE", "0") == "1"
)

def print(self, *args, **kwargs):
if not self.verbose:
Expand All @@ -137,11 +78,8 @@ def get_package_spec(self, name, version=None):
return spec

def configure_default_packages(self, options: dict, targets: List[str]):
from ltchiptool.util.dict import RecursiveDict
from libretiny import RecursiveDict

self.verbose = (
"-v" in sys.argv or "--verbose" in sys.argv or "PIOVERBOSE=1" in sys.argv
)
self.print(f"configure_default_packages(targets={targets})")

pioframework = options.get("pioframework") or ["base"]
Expand Down Expand Up @@ -298,19 +236,19 @@ def get_boards(self, id_=None):
return result

def update_board(self, board: PlatformBoardConfig):
from libretiny import Board, Family, merge_dicts

if "_base" in board:
board._manifest = ltchiptool.Board.get_data(board._manifest)
board._manifest = Board.get_data(board._manifest)
board._manifest.pop("_base")

if self.custom("board"):
from ltchiptool.util.dict import merge_dicts

with open(self.custom("board"), "r") as f:
custom_board = json.load(f)
board._manifest = merge_dicts(board._manifest, custom_board)

family = board.get("build.family")
family = ltchiptool.Family.get(short_name=family)
family = Family.get(short_name=family)
# add "frameworks" key with the default "base"
board.manifest["frameworks"] = ["base"]
# add "arduino" framework if supported
Expand Down
14 changes: 14 additions & 0 deletions tools/libretiny/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Kuba Szczodrzyński 2023-09-07.

from .board import Board
from .dict import RecursiveDict, merge_dicts
from .family import Family

# TODO refactor and remove all this from here

__all__ = [
"Board",
"Family",
"RecursiveDict",
"merge_dicts",
]
34 changes: 34 additions & 0 deletions tools/libretiny/board.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (c) Kuba Szczodrzyński 2022-07-29.

from typing import Union

from genericpath import isfile

from .dict import merge_dicts
from .fileio import readjson
from .lvm import lvm_load_json


class Board:
@staticmethod
def get_data(board: Union[str, dict]) -> dict:
if not isinstance(board, dict):
if isfile(board):
board = readjson(board)
if not board:
raise FileNotFoundError(f"Board not found: {board}")
else:
source = board
board = lvm_load_json(f"boards/{board}.json")
board["source"] = source
if "_base" in board:
base = board["_base"]
if not isinstance(base, list):
base = [base]
result = {}
for base_name in base:
board_base = lvm_load_json(f"boards/_base/{base_name}.json")
merge_dicts(result, board_base)
merge_dicts(result, board)
board = result
return board
Loading

0 comments on commit 0f5d0a8

Please sign in to comment.