Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7def5108ea | ||
|
|
01be1e437d | ||
|
|
0a414a2342 | ||
|
|
7fc4630f1d | ||
|
|
1522b71317 | ||
|
|
45b0cafa2b | ||
|
|
897d6ddbe8 | ||
|
|
7bc3e74309 | ||
|
|
4da4afa879 | ||
|
|
9358afeb47 | ||
|
|
97bb660d5a | ||
|
|
03c93a553d | ||
|
|
96716ae034 | ||
|
|
41516d60bf | ||
|
|
1047b67c97 | ||
|
|
dddedd92ba | ||
|
|
7d12b16fba |
3 changed files with 211 additions and 34 deletions
|
|
@ -12,7 +12,7 @@ repos:
|
|||
hooks:
|
||||
- id: reuse
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Implementation Notes
|
|||
# imports
|
||||
import sys
|
||||
import os
|
||||
import stat
|
||||
import shutil
|
||||
import subprocess
|
||||
import fcntl
|
||||
|
|
@ -37,6 +38,41 @@ import adafruit_platformdetect
|
|||
__version__ = "0.0.0+auto.0"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_Python_Shell.git"
|
||||
|
||||
# This must be by order of release
|
||||
RASPI_VERSIONS = (
|
||||
"wheezy",
|
||||
"jessie",
|
||||
"stretch",
|
||||
"buster",
|
||||
"bullseye",
|
||||
"bookworm",
|
||||
"trixie",
|
||||
)
|
||||
|
||||
WINDOW_MANAGERS = {
|
||||
"x11": "W1",
|
||||
"wayfire": "W2",
|
||||
"labwc": "W3",
|
||||
}
|
||||
|
||||
FILE_MODES = {
|
||||
"+x": stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
|
||||
"+r": stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH,
|
||||
"+w": stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH,
|
||||
"a+x": stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
|
||||
"a+r": stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH,
|
||||
"a+w": stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH,
|
||||
"u+x": stat.S_IXUSR,
|
||||
"u+r": stat.S_IRUSR,
|
||||
"u+w": stat.S_IWUSR,
|
||||
"g+x": stat.S_IXGRP,
|
||||
"g+r": stat.S_IRGRP,
|
||||
"g+w": stat.S_IWGRP,
|
||||
"o+x": stat.S_IXOTH,
|
||||
"o+r": stat.S_IROTH,
|
||||
"o+w": stat.S_IWOTH,
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Shell:
|
||||
|
|
@ -125,6 +161,50 @@ class Shell:
|
|||
return False
|
||||
return True
|
||||
|
||||
def write_templated_file(self, output_path, template, **kwargs):
|
||||
"""
|
||||
Use a template file and render it with the given context and write it to the specified path.
|
||||
The template file should contain placeholders in the format {key} which will be replaced
|
||||
with the corresponding values from the kwargs dictionary.
|
||||
"""
|
||||
# if path is an existing directory, the template filename will be used
|
||||
output_path = self.path(output_path)
|
||||
if os.path.isdir(output_path):
|
||||
output_path = os.path.join(output_path, os.path.basename(template))
|
||||
|
||||
# Render the template with the provided context
|
||||
rendered_content = self.load_template(template, **kwargs)
|
||||
|
||||
if rendered_content is None:
|
||||
self.error(
|
||||
f"Failed to load template '{template}'. Unable to write file '{output_path}'."
|
||||
)
|
||||
return False
|
||||
|
||||
append = kwargs.get("append", False)
|
||||
self.write_text_file(output_path, rendered_content, append=append)
|
||||
|
||||
return True
|
||||
|
||||
def load_template(self, template, **kwargs):
|
||||
"""
|
||||
Load a template file and return its content with the placeholders replaced by the provided
|
||||
context. The template file should contain placeholders in the format {key} which will be
|
||||
replaced with the corresponding values from the kwargs dictionary.
|
||||
"""
|
||||
if not os.path.exists(template):
|
||||
self.error(f"Template file '{template}' does not exist")
|
||||
return None
|
||||
|
||||
with open(template, "r") as template_file:
|
||||
template_content = template_file.read()
|
||||
|
||||
# Render the template with the provided context
|
||||
for key, value in kwargs.items():
|
||||
template_content = template_content.replace(f"{{{key}}}", str(value))
|
||||
|
||||
return template_content
|
||||
|
||||
def info(self, message, **kwargs):
|
||||
"""
|
||||
Display a message with the group in green
|
||||
|
|
@ -304,7 +384,10 @@ class Shell:
|
|||
# Not found; append (silently)
|
||||
self.write_text_file(file, replacement, append=True)
|
||||
|
||||
def pattern_search(self, location, pattern, multi_line=False, return_match=False):
|
||||
# pylint: disable=too-many-arguments
|
||||
def pattern_search(
|
||||
self, location, pattern, multi_line=False, return_match=False, find_all=False
|
||||
):
|
||||
"""
|
||||
Similar to grep, but uses pure python
|
||||
multi_line will search the entire file as a large text glob,
|
||||
|
|
@ -314,16 +397,17 @@ class Shell:
|
|||
"""
|
||||
location = self.path(location)
|
||||
found = False
|
||||
search_function = re.findall if find_all else re.search
|
||||
|
||||
if self.exists(location) and not self.isdir(location):
|
||||
if multi_line:
|
||||
with open(location, "r+", encoding="utf-8") as file:
|
||||
match = re.search(pattern, file.read(), flags=re.DOTALL)
|
||||
match = search_function(pattern, file.read(), flags=re.DOTALL)
|
||||
if match:
|
||||
found = True
|
||||
else:
|
||||
for line in fileinput.FileInput(location):
|
||||
match = re.search(pattern, line)
|
||||
match = search_function(pattern, line)
|
||||
if match:
|
||||
found = True
|
||||
break
|
||||
|
|
@ -400,8 +484,13 @@ class Shell:
|
|||
Change the permissions of a file or directory
|
||||
"""
|
||||
location = self.path(location)
|
||||
# Convert a text mode to an integer mode
|
||||
if isinstance(mode, str):
|
||||
if mode not in FILE_MODES:
|
||||
raise ValueError(f"Invalid mode string '{mode}'")
|
||||
mode = FILE_MODES[mode]
|
||||
if not 0 <= mode <= 0o777:
|
||||
raise ValueError("Invalid mode value")
|
||||
raise ValueError(f"Invalid mode value '{mode}'")
|
||||
if os.path.exists(location):
|
||||
os.chmod(location, mode)
|
||||
|
||||
|
|
@ -458,6 +547,16 @@ class Shell:
|
|||
with open(self.path(path), mode, encoding="utf-8") as service_file:
|
||||
service_file.write(content)
|
||||
|
||||
def read_text_file(self, path):
|
||||
"""
|
||||
Read the contents of a file at the specified path
|
||||
"""
|
||||
path = self.path(path)
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"File '{path}' does not exist")
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
return file.read()
|
||||
|
||||
@staticmethod
|
||||
def is_python3():
|
||||
"Check if we are running Python 3 or later"
|
||||
|
|
@ -555,24 +654,29 @@ class Shell:
|
|||
"""Return a string containing the raspbian version"""
|
||||
if self.get_os() != "Raspbian":
|
||||
return None
|
||||
raspbian_releases = (
|
||||
"bookworm",
|
||||
"bullseye",
|
||||
"buster",
|
||||
"stretch",
|
||||
"jessie",
|
||||
"wheezy",
|
||||
)
|
||||
if os.path.exists("/etc/os-release"):
|
||||
with open("/etc/os-release", encoding="utf-8") as f:
|
||||
release_file = f.read()
|
||||
if "/sid" in release_file:
|
||||
return "unstable"
|
||||
for raspbian in raspbian_releases:
|
||||
for raspbian in RASPI_VERSIONS:
|
||||
if raspbian in release_file:
|
||||
return raspbian
|
||||
return None
|
||||
|
||||
def is_minumum_version(self, version):
|
||||
"""Check if the version is at least the specified version"""
|
||||
# Check that version is a string
|
||||
if not isinstance(version, str):
|
||||
raise ValueError("Version must be a string")
|
||||
# Check that version is in the list of valid versions
|
||||
if version.lower() not in RASPI_VERSIONS:
|
||||
raise ValueError("Invalid version")
|
||||
# Check that the current version is at least the specified version
|
||||
return RASPI_VERSIONS.index(
|
||||
self.get_raspbian_version()
|
||||
) >= RASPI_VERSIONS.index(version.lower())
|
||||
|
||||
def prompt_reboot(self, default="y", **kwargs):
|
||||
"""Prompt the user for a reboot"""
|
||||
if not self.prompt("REBOOT NOW?", default=default, **kwargs):
|
||||
|
|
@ -592,21 +696,87 @@ class Shell:
|
|||
)
|
||||
self.prompt_reboot()
|
||||
|
||||
def check_kernel_userspace_mismatch(self):
|
||||
def check_kernel_userspace_mismatch(self, attempt_fix=True, fix_with_x11=False):
|
||||
"""
|
||||
Check if the userspace is 64-bit and kernel is 32-bit
|
||||
"""
|
||||
if self.is_arm64() and platform.architecture()[0] == "32bit":
|
||||
if self.is_kernel_userspace_mismatched():
|
||||
print(
|
||||
"Unable to compile driver because kernel space is 64-bit, but user space is 32-bit."
|
||||
)
|
||||
if self.is_raspberry_pi_os() and self.prompt(
|
||||
"Add parameter to /boot/config.txt to use 32-bit kernel?"
|
||||
config = self.get_boot_config()
|
||||
if (
|
||||
self.is_raspberry_pi_os()
|
||||
and attempt_fix
|
||||
and config
|
||||
and self.prompt(f"Add parameter to {config} to use 32-bit kernel?")
|
||||
):
|
||||
self.reconfig("/boot/config.txt", "^.*arm_64bit.*$", "arm_64bit=0")
|
||||
# Set to use 32-bit kernel
|
||||
self.reconfig(config, "^.*arm_64bit.*$", "arm_64bit=0")
|
||||
if fix_with_x11:
|
||||
self.set_window_manager("x11")
|
||||
self.prompt_reboot()
|
||||
else:
|
||||
self.bail("Unable to continue while mismatch is present.")
|
||||
raise RuntimeError("Unable to continue while mismatch is present.")
|
||||
|
||||
def set_window_manager(self, manager):
|
||||
"""
|
||||
Call raspi-config to set a new window manager
|
||||
"""
|
||||
if not self.is_minumum_version("bullseye"):
|
||||
return
|
||||
|
||||
if manager.lower() not in WINDOW_MANAGERS:
|
||||
raise ValueError("Invalid window manager")
|
||||
|
||||
if manager.lower() == "labwc" and not self.exists("/usr/bin/labwc"):
|
||||
raise RuntimeError("labwc is not installed")
|
||||
|
||||
print(f"Using {manager} as the window manager")
|
||||
if not self.run_command(
|
||||
"sudo raspi-config nonint do_wayland " + WINDOW_MANAGERS[manager.lower()]
|
||||
):
|
||||
raise RuntimeError("Unable to change window manager")
|
||||
|
||||
def get_window_manager(self):
|
||||
"""
|
||||
Get the current window manager
|
||||
"""
|
||||
sessions = {"wayfire": "LXDE-pi-wayfire"}
|
||||
# Check for Raspbian Desktop sessions
|
||||
if self.exists("/usr/share/xsessions/rpd-x.desktop") or self.exists(
|
||||
"/usr/share/wayland-sessions/rpd-labwc.desktop"
|
||||
):
|
||||
sessions.update({"x11": "rpd-x", "labwc": "rpd-labwc"})
|
||||
else:
|
||||
sessions.update({"x11": "LXDE-pi-x", "labwc": "LXDE-pi-labwc"})
|
||||
|
||||
matches = self.pattern_search(
|
||||
"/etc/lightdm/lightdm.conf", "^(?!#.*?)user-session=(.+)", False, True
|
||||
)
|
||||
if matches:
|
||||
session_match = matches.group(1)
|
||||
for key, session in sessions.items():
|
||||
if session_match == session:
|
||||
return key
|
||||
return None
|
||||
|
||||
def get_boot_config(self):
|
||||
"""
|
||||
Get the location of the boot config file
|
||||
"""
|
||||
# check if /boot/firmware/config.txt exists
|
||||
if self.exists("/boot/firmware/config.txt"):
|
||||
return "/boot/firmware/config.txt"
|
||||
if self.exists("/boot/config.txt"):
|
||||
return "/boot/config.txt"
|
||||
return None
|
||||
|
||||
def is_kernel_userspace_mismatched(self):
|
||||
"""
|
||||
If the userspace 64-bit and kernel is 32-bit?
|
||||
"""
|
||||
return self.is_arm64() and platform.architecture()[0] == "32bit"
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
|
|
@ -632,6 +802,14 @@ class Shell:
|
|||
detector = adafruit_platformdetect.Detector()
|
||||
return detector.board.id
|
||||
|
||||
@staticmethod
|
||||
def is_pi5_or_newer():
|
||||
"""
|
||||
Use PlatformDetect to check if this is a Raspberry Pi 5 or newer
|
||||
"""
|
||||
detector = adafruit_platformdetect.Detector()
|
||||
return detector.board.any_raspberry_pi_5_board
|
||||
|
||||
@staticmethod
|
||||
def get_architecture():
|
||||
"""
|
||||
|
|
|
|||
27
docs/conf.py
27
docs/conf.py
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
|
|
@ -16,13 +17,14 @@ sys.path.insert(0, os.path.abspath(".."))
|
|||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinxcontrib.jquery",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.todo",
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3.4", None),
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
"CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +38,14 @@ master_doc = "index"
|
|||
|
||||
# General information about the project.
|
||||
project = "Adafruit Shell Library"
|
||||
copyright = "2020 Melissa LeBlanc-Williams"
|
||||
creation_year = "2020"
|
||||
current_year = str(datetime.datetime.now().year)
|
||||
year_duration = (
|
||||
current_year
|
||||
if current_year == creation_year
|
||||
else creation_year + " - " + current_year
|
||||
)
|
||||
copyright = year_duration + " Melissa LeBlanc-Williams"
|
||||
author = "Melissa LeBlanc-Williams"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
|
@ -91,19 +100,9 @@ napoleon_numpy_docstring = False
|
|||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
|
||||
import sphinx_rtd_theme
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
|
||||
except:
|
||||
html_theme = "default"
|
||||
html_theme_path = ["."]
|
||||
else:
|
||||
html_theme_path = ["."]
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
|
|
|
|||
Loading…
Reference in a new issue