Compare commits

...

17 commits
1.8.1 ... main

Author SHA1 Message Date
Melissa LeBlanc-Williams
7def5108ea
Merge pull request #27 from makermelissa/main
New features required for installer script changes
2025-08-06 15:07:47 -07:00
Melissa LeBlanc-Williams
01be1e437d Allow finding multiple items 2025-08-06 14:58:27 -07:00
Melissa LeBlanc-Williams
0a414a2342 Add read_text_file function 2025-08-06 13:50:52 -07:00
Melissa LeBlanc-Williams
7fc4630f1d Fix wayland dm detection 2025-08-06 11:39:58 -07:00
Melissa LeBlanc-Williams
1522b71317 Use class write funtion for appending 2025-08-06 10:13:38 -07:00
Melissa LeBlanc-Williams
45b0cafa2b Fix error from pre-commit suggestion 2025-08-04 12:52:49 -07:00
Melissa LeBlanc-Williams
897d6ddbe8 Add window manager detection 2025-08-04 12:41:42 -07:00
Melissa LeBlanc-Williams
7bc3e74309 Fixed bug introduced by pylint suggestion 2025-07-18 15:16:27 -07:00
Melissa LeBlanc-Williams
4da4afa879 Add template functions and chmod string support 2025-07-18 13:21:02 -07:00
Melissa LeBlanc-Williams
9358afeb47
Merge pull request #26 from makermelissa/main
Use new platform detect feature to see if board is Pi5
2025-03-04 12:17:25 -08:00
Melissa LeBlanc-Williams
97bb660d5a Merge branch 'main' of https://github.com/adafruit/Adafruit_Python_Shell 2025-03-04 12:15:42 -08:00
Melissa LeBlanc-Williams
03c93a553d Use platform detect to see if board is Pi5 2025-03-04 12:15:29 -08:00
Limor "Ladyada" Fried
96716ae034
Merge pull request #25 from makermelissa/main
Add some useful functions for wayland detection
2025-03-02 18:48:45 -05:00
Melissa LeBlanc-Williams
41516d60bf Update docs config and pre-commit version 2025-03-01 13:50:40 -08:00
Melissa LeBlanc-Williams
1047b67c97 Update doc config 2025-03-01 13:42:48 -08:00
Melissa LeBlanc-Williams
dddedd92ba Run pre-commit 2025-03-01 13:37:12 -08:00
Melissa LeBlanc-Williams
7d12b16fba Add some useful functions for wayland detection 2025-03-01 13:21:40 -08:00
3 changed files with 211 additions and 34 deletions

View file

@ -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

View file

@ -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():
"""

View file

@ -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,