Compare commits

...

11 commits

Author SHA1 Message Date
3062378a7b Merge branch 'master' of https://github.com/Arksine/moonraker 2026-01-17 22:45:51 +01:00
Eric Callahan
bac55c65f8
workflows: delete stale github workflow
The "zip" style distribution of Moonraker was never deployed.

Signed-off-by: Eric Callahan <arksine.code@gmail.com>
2026-01-15 13:00:38 +00:00
Eric Callahan
63672ea38b
mqtt: update static types to support client version 2.0 or greater
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-15 12:55:40 +00:00
Eric Callahan
4b751e79e1
build: update universal zeroconf wheels
Version 0.136.2 is the last release supporting Python 3.8,  0.148.0 is the latest release at the time of this commit.  These provide fallback options for
platforms for which wheels are not available.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-15 00:26:27 +00:00
Eric Callahan
3ea83163b0 build: update python dependencies to their latest versions
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-14 16:21:49 +00:00
Eric Callahan
0e8dd923f8 install: add support for dev container installs
Add support for installing Moonraker inside a vscode "python"
dev container.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-14 12:49:16 +00:00
Eric Callahan
2b1c70a9e1 build: update python dev requirements
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-14 12:49:16 +00:00
Eric Callahan
d7df2876dd sysdeps_parser: add support for env exclusions
A list of system package exclusions may be set via the
MOONRAKER_EXCLUDED_PKGS environment variable.
In addition, a MOONRAKER_VENDOR enviroment
variable has been added to allow for manually setting
the vendor name.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-14 12:49:16 +00:00
Eric Callahan
3e6378fbc6 repo: update gitignore file
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
2026-01-14 12:49:16 +00:00
Eric Callahan
bdeddcabad system_deploy: check update severity bits in info_code
The update severity is stored in the upper 16 bits of the info code in
newer versions of package kit.  Some backends may omit the lower
16-bits and only store the severity, in those cases use the update
severity enum value in place of the info value.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2026-01-14 12:49:16 +00:00
82ce1ff26f feat: add spoolman auth support 2026-01-14 06:22:14 +01:00
12 changed files with 185 additions and 145 deletions

View file

@ -1,51 +0,0 @@
# CI Code for generating and publishing beta assets
name: publish_assets
on:
release:
types: [published]
jobs:
generate_assets:
runs-on: ubuntu-latest
steps:
- name: Checkout Moonraker
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.ref }}
path: moonraker
- name: Checkout Klipper
uses: actions/checkout@v2
with:
fetch-depth: 0
repository: Klipper3d/klipper
path: klipper
- name: Build Beta Assets
if: ${{ github.event.release.prerelease }}
run: >
./moonraker/scripts/build-zip-release.sh -b
-o ${{ github.workspace }}
-k ${{ github.workspace }}/klipper
- name: Build Stable Assets
if: ${{ !github.event.release.prerelease }}
run: >
./moonraker/scripts/build-zip-release.sh
-o ${{ github.workspace }}
-k ${{ github.workspace }}/klipper
- name: Upload assets
run: |
cd moonraker
gh release upload ${{ env.TAG }} ${{ env.FILES }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FILES: >
${{ github.workspace }}/moonraker.zip
${{ github.workspace }}/klipper.zip
${{ github.workspace }}/RELEASE_INFO
${{ github.workspace }}/COMMIT_LOG
TAG: ${{ github.event.release.tag_name }}

6
.gitignore vendored
View file

@ -8,7 +8,13 @@ __pycache__/
venv venv
start_moonraker start_moonraker
*.env *.env
*.egg-info
.pdm-python .pdm-python
build build
dist dist
share share
.vscode
.mypy_cache
.pdm-build
.pytest_cache

View file

@ -21,6 +21,10 @@ from ..common import (
KlippyState KlippyState
) )
from ..utils import json_wrapper as jsonw from ..utils import json_wrapper as jsonw
try:
from paho.mqtt.reasoncodes import ReasonCode
except ImportError:
from paho.mqtt.reasoncodes import ReasonCodes as ReasonCode
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -41,6 +45,7 @@ if TYPE_CHECKING:
from ..common import JsonRPC, APIDefinition from ..common import JsonRPC, APIDefinition
from ..eventloop import FlexTimer from ..eventloop import FlexTimer
from .klippy_apis import KlippyAPI from .klippy_apis import KlippyAPI
from paho.mqtt.properties import Properties
FlexCallback = Callable[[bytes], Optional[Coroutine]] FlexCallback = Callable[[bytes], Optional[Coroutine]]
RPCCallback = Callable[..., Coroutine] RPCCallback = Callable[..., Coroutine]
@ -73,7 +78,7 @@ class ExtPahoClient(paho_mqtt.Client):
"remaining_count": [], "remaining_count": [],
"remaining_mult": 1, "remaining_mult": 1,
"remaining_length": 0, "remaining_length": 0,
"packet": b"", "packet": b"", # type: ignore
"to_process": 0, "to_process": 0,
"pos": 0 "pos": 0
} }
@ -100,7 +105,7 @@ class ExtPahoClient(paho_mqtt.Client):
self._last_msg_out = paho_mqtt.time_func() self._last_msg_out = paho_mqtt.time_func()
self._ping_t = 0 self._ping_t = 0
self._state = paho_mqtt.mqtt_cs_new self._state = paho_mqtt.mqtt_cs_new # type: ignore
self._sock_close() self._sock_close()
@ -149,16 +154,16 @@ class ExtPahoClient(paho_mqtt.Client):
if self._transport == "websockets": if self._transport == "websockets":
sock.settimeout(self._keepalive) sock.settimeout(self._keepalive)
sock = paho_mqtt.WebsocketWrapper( sock = paho_mqtt.WebsocketWrapper( # type: ignore
sock, self._host, self._port, self._ssl, sock, self._host, self._port, self._ssl,
self._websocket_path, self._websocket_extra_headers self._websocket_path, self._websocket_extra_headers
) # type: ignore ) # type: ignore
self._sock = sock self._sock = sock # type: ignore
assert self._sock is not None assert self._sock is not None
self._sock.setblocking(False) self._sock.setblocking(False)
self._registered_write = False self._registered_write = False
self._call_socket_open() self._call_socket_open() # type: ignore
return self._send_connect(self._keepalive) return self._send_connect(self._keepalive)
@ -233,7 +238,7 @@ class BrokerAckLogger:
def __call__(self, fut: asyncio.Future) -> None: def __call__(self, fut: asyncio.Future) -> None:
if self.action == "subscribe": if self.action == "subscribe":
res: Union[List[int], List[paho_mqtt.ReasonCodes]] res: Union[List[int], List[ReasonCode]]
res = fut.result() res = fut.result()
log_msg = "MQTT Subscriptions Acknowledged" log_msg = "MQTT Subscriptions Acknowledged"
if len(res) != len(self.topics): if len(res) != len(self.topics):
@ -243,7 +248,7 @@ class BrokerAckLogger:
else: else:
for topic, qos in zip(self.topics, res): for topic, qos in zip(self.topics, res):
log_msg += f"\n Topic: {topic} | " log_msg += f"\n Topic: {topic} | "
if isinstance(qos, paho_mqtt.ReasonCodes): if isinstance(qos, ReasonCode):
log_msg += qos.getName() log_msg += qos.getName()
else: else:
log_msg += f"Granted QoS {qos}" log_msg += f"Granted QoS {qos}"
@ -265,41 +270,44 @@ class AIOHelper:
self.client.on_socket_open = self._on_socket_open self.client.on_socket_open = self._on_socket_open
self.client.on_socket_close = self._on_socket_close self.client.on_socket_close = self._on_socket_close
self.client._on_socket_register_write = self._on_socket_register_write self.client._on_socket_register_write = self._on_socket_register_write
self.client._on_socket_unregister_write = \ self.client._on_socket_unregister_write = self._on_socket_unregister_write
self._on_socket_unregister_write
self.misc_task: Optional[asyncio.Task] = None self.misc_task: Optional[asyncio.Task] = None
def _on_socket_open(self, def _on_socket_open(
client: paho_mqtt.Client, self,
userdata: Any, client: paho_mqtt.Client,
sock: socket.socket userdata: Any,
) -> None: sock: socket.socket | paho_mqtt.SocketLike
) -> None:
logging.info("MQTT Socket Opened") logging.info("MQTT Socket Opened")
self.loop.add_reader(sock, client.loop_read) self.loop.add_reader(sock, client.loop_read)
self.misc_task = self.loop.create_task(self.misc_loop()) self.misc_task = self.loop.create_task(self.misc_loop())
def _on_socket_close(self, def _on_socket_close(
client: paho_mqtt.Client, self,
userdata: Any, client: paho_mqtt.Client,
sock: socket.socket userdata: Any,
) -> None: sock: socket.socket | paho_mqtt.SocketLike
) -> None:
logging.info("MQTT Socket Closed") logging.info("MQTT Socket Closed")
self.loop.remove_reader(sock) self.loop.remove_reader(sock)
if self.misc_task is not None: if self.misc_task is not None:
self.misc_task.cancel() self.misc_task.cancel()
def _on_socket_register_write(self, def _on_socket_register_write(
client: paho_mqtt.Client, self,
userdata: Any, client: paho_mqtt.Client,
sock: socket.socket userdata: Any,
) -> None: sock: socket.socket | paho_mqtt.SocketLike
) -> None:
self.loop.add_writer(sock, client.loop_write) self.loop.add_writer(sock, client.loop_write)
def _on_socket_unregister_write(self, def _on_socket_unregister_write(
client: paho_mqtt.Client, self,
userdata: Any, client: paho_mqtt.Client,
sock: socket.socket userdata: Any,
) -> None: sock: socket.socket | paho_mqtt.SocketLike
) -> None:
self.loop.remove_writer(sock) self.loop.remove_writer(sock)
async def misc_loop(self) -> None: async def misc_loop(self) -> None:
@ -354,7 +362,9 @@ class MQTTClient(APITransport):
config.getboolean("publish_split_status", False) config.getboolean("publish_split_status", False)
client_id: str = config.get("client_id", "") client_id: str = config.get("client_id", "")
if PAHO_MQTT_VERSION < (2, 0): if PAHO_MQTT_VERSION < (2, 0):
self.client = ExtPahoClient(client_id, protocol=self.protocol) self.client = ExtPahoClient(
client_id, protocol=self.protocol # type: ignore
)
else: else:
self.client = ExtPahoClient( self.client = ExtPahoClient(
paho_mqtt.CallbackAPIVersion.VERSION1, client_id, # type: ignore paho_mqtt.CallbackAPIVersion.VERSION1, client_id, # type: ignore
@ -472,7 +482,7 @@ class MQTTClient(APITransport):
self._publish_status_update(payload, self.last_status_time) self._publish_status_update(payload, self.last_status_time)
def _on_message(self, def _on_message(self,
client: str, client: str | paho_mqtt.Client,
user_data: Any, user_data: Any,
message: paho_mqtt.MQTTMessage message: paho_mqtt.MQTTMessage
) -> None: ) -> None:
@ -491,8 +501,8 @@ class MQTTClient(APITransport):
client: paho_mqtt.Client, client: paho_mqtt.Client,
user_data: Any, user_data: Any,
flags: Dict[str, Any], flags: Dict[str, Any],
reason_code: Union[int, paho_mqtt.ReasonCodes], reason_code: Union[int, ReasonCode],
properties: Optional[paho_mqtt.Properties] = None properties: Optional[Properties] = None
) -> None: ) -> None:
logging.info("MQTT Client Connected") logging.info("MQTT Client Connected")
if reason_code == 0: if reason_code == 0:
@ -521,7 +531,7 @@ class MQTTClient(APITransport):
client: paho_mqtt.Client, client: paho_mqtt.Client,
user_data: Any, user_data: Any,
reason_code: int, reason_code: int,
properties: Optional[paho_mqtt.Properties] = None properties: Optional[Properties] = None
) -> None: ) -> None:
if self.disconnect_evt is not None: if self.disconnect_evt is not None:
self.disconnect_evt.set() self.disconnect_evt.set()
@ -547,8 +557,8 @@ class MQTTClient(APITransport):
client: paho_mqtt.Client, client: paho_mqtt.Client,
user_data: Any, user_data: Any,
msg_id: int, msg_id: int,
flex: Union[List[int], List[paho_mqtt.ReasonCodes]], flex: Union[List[int], List[ReasonCode]],
properties: Optional[paho_mqtt.Properties] = None properties: Optional[Properties] = None
) -> None: ) -> None:
sub_fut = self.pending_acks.pop(msg_id, None) sub_fut = self.pending_acks.pop(msg_id, None)
if sub_fut is not None and not sub_fut.done(): if sub_fut is not None and not sub_fut.done():
@ -558,8 +568,8 @@ class MQTTClient(APITransport):
client: paho_mqtt.Client, client: paho_mqtt.Client,
user_data: Any, user_data: Any,
msg_id: int, msg_id: int,
properties: Optional[paho_mqtt.Properties] = None, properties: Optional[Properties] = None,
reasoncodes: Optional[paho_mqtt.ReasonCodes] = None reasoncodes: Optional[ReasonCode] = None
) -> None: ) -> None:
unsub_fut = self.pending_acks.pop(msg_id, None) unsub_fut = self.pending_acks.pop(msg_id, None)
if unsub_fut is not None and not unsub_fut.done(): if unsub_fut is not None and not unsub_fut.done():

View file

@ -9,7 +9,9 @@ import asyncio
import logging import logging
import re import re
import contextlib import contextlib
import base64
import tornado.websocket as tornado_ws import tornado.websocket as tornado_ws
from tornado.httpclient import HTTPRequest
from tornado import version_info as tornado_version from tornado import version_info as tornado_version
from ..common import RequestType, HistoryFieldData from ..common import RequestType, HistoryFieldData
from ..utils import json_wrapper as jsonw from ..utils import json_wrapper as jsonw
@ -86,6 +88,23 @@ class SpoolManager:
self.spoolman_url = f"{scheme}://{host}/api" self.spoolman_url = f"{scheme}://{host}/api"
self.ws_url = f"{ws_scheme}://{host}/api/v1/spool" self.ws_url = f"{ws_scheme}://{host}/api/v1/spool"
headers_raw = config.get("headers", "")
self.http_headers = {}
for c in headers_raw.split(";"):
c = c.strip()
if not c:
continue
c_parts = c.split(":", 1)
if len(c_parts) != 2:
raise config.error(f"Section [spoolman], Option headers: {c}: Invalid header format")
self.http_headers[c_parts[0]] = c_parts[1]
username = config.get("http_username", None)
password = config.get("http_password", None)
if username and password:
creds = base64.b64encode(f"{username}:{password}".encode()).decode()
self.http_headers["Authorization"] = "Basic " + creds
def _register_notifications(self): def _register_notifications(self):
self.server.register_notification("spoolman:active_spool_set") self.server.register_notification("spoolman:active_spool_set")
self.server.register_notification("spoolman:spoolman_status_changed") self.server.register_notification("spoolman:spoolman_status_changed")
@ -130,10 +149,10 @@ class SpoolManager:
if log_connect: if log_connect:
logging.info(f"Connecting To Spoolman: {self.ws_url}") logging.info(f"Connecting To Spoolman: {self.ws_url}")
log_connect = False log_connect = False
request = HTTPRequest(url=self.ws_url, headers=self.http_headers, connect_timeout=5.)
try: try:
self.spoolman_ws = await tornado_ws.websocket_connect( self.spoolman_ws = await tornado_ws.websocket_connect(
self.ws_url, request,
connect_timeout=5.,
ping_interval=None if tornado_version < (6, 5) else 20. ping_interval=None if tornado_version < (6, 5) else 20.
) )
setattr(self.spoolman_ws, "on_ping", self._on_ws_ping) setattr(self.spoolman_ws, "on_ping", self._on_ws_ping)
@ -217,7 +236,7 @@ class SpoolManager:
if self.spool_id is not None: if self.spool_id is not None:
response = await self.http_client.get( response = await self.http_client.get(
f"{self.spoolman_url}/v1/spool/{self.spool_id}", f"{self.spoolman_url}/v1/spool/{self.spool_id}",
connect_timeout=1., request_timeout=2. connect_timeout=1., request_timeout=2., headers=self.http_headers
) )
if response.status_code == 404: if response.status_code == 404:
logging.info(f"Spool ID {self.spool_id} not found, setting to None") logging.info(f"Spool ID {self.spool_id} not found, setting to None")
@ -308,7 +327,8 @@ class SpoolManager:
response = await self.http_client.request( response = await self.http_client.request(
method="PUT", method="PUT",
url=f"{self.spoolman_url}/v1/spool/{spool_id}/use", url=f"{self.spoolman_url}/v1/spool/{spool_id}/use",
body={"use_length": used_length} body={"use_length": used_length},
headers=self.http_headers
) )
if response.has_error(): if response.has_error():
if response.status_code == 404: if response.status_code == 404:
@ -370,6 +390,7 @@ class SpoolManager:
method=method, method=method,
url=full_url, url=full_url,
body=body, body=body,
headers=self.http_headers
) )
if not use_v2_response: if not use_v2_response:
response.raise_for_status() response.raise_for_status()

View file

@ -392,6 +392,9 @@ class PackageKitTransaction:
summary: str summary: str
) -> None: ) -> None:
info = PkEnum.Info.from_index(info_code & 0xFFFF) info = PkEnum.Info.from_index(info_code & 0xFFFF)
severity = PkEnum.Info.from_index((info_code >> 16) & 0xFFFF)
if info == PkEnum.Info.UNKNOWN:
info = severity
if self._role in self.GET_PKG_ROLES: if self._role in self.GET_PKG_ROLES:
pkg_data = { pkg_data = {
'package_id': package_id, 'package_id': package_id,

View file

@ -4,6 +4,7 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license # This file may be distributed under the terms of the GNU GPLv3 license
from __future__ import annotations from __future__ import annotations
import os
import shlex import shlex
import re import re
import pathlib import pathlib
@ -61,15 +62,22 @@ class SysDepsParser:
version = distro_info.get("distro_version") version = distro_info.get("distro_version")
if version: if version:
self.distro_version = _convert_version(version) self.distro_version = _convert_version(version)
self.vendor: str = "" self.vendor: str = os.getenv("MOONRAKER_VENDOR", "")
if pathlib.Path("/etc/rpi-issue").is_file(): if not self.vendor and pathlib.Path("/etc/rpi-issue").is_file():
self.vendor = "raspberry-pi" self.vendor = "raspberry-pi"
exclusions = os.getenv("MOONRAKER_EXCLUDED_PKGS", "")
self.exclusions: List[str] = [
excl.strip() for excl in exclusions.split() if excl.strip()
]
def _parse_spec(self, full_spec: str) -> str | None: def _parse_spec(self, full_spec: str) -> str | None:
parts = full_spec.split(";", maxsplit=1) parts = full_spec.split(";", maxsplit=1)
if len(parts) == 1:
return full_spec
pkg_name = parts[0].strip() pkg_name = parts[0].strip()
if pkg_name in self.exclusions or not pkg_name:
logging.info(f"Package '{full_spec}' excluded by environment")
return None
if len(parts) == 1:
return pkg_name
expressions = re.split(r"( and | or )", parts[1].strip()) expressions = re.split(r"( and | or )", parts[1].strip())
if not len(expressions) & 1: if not len(expressions) & 1:
# There should always be an odd number of expressions. Each # There should always be an odd number of expressions. Each

View file

@ -6,23 +6,22 @@ authors = [
{name = "Eric Callahan", email = "arksine.code@gmail.com"}, {name = "Eric Callahan", email = "arksine.code@gmail.com"},
] ]
dependencies = [ dependencies = [
"tornado>=6.2.0, <=6.5.1", "tornado>=6.2.0, <=6.5.4",
"pyserial==3.4", "pyserial==3.4",
"pillow>=9.5.0, <=11.1.0", "pillow>=9.5.0, <=12.1.0",
"streaming-form-data>=1.11.0, <=1.19.1", "streaming-form-data>=1.11.0, <=1.19.1",
"distro==1.9.0", "distro==1.9.0",
"inotify-simple==1.3.5", "inotify-simple==2.0.1",
"libnacl==2.1.0", "libnacl==2.1.0",
"paho-mqtt==1.6.1", "paho-mqtt==2.1.0",
"zeroconf==0.131.0", "zeroconf>=0.131.0, <=0.148.0",
"preprocess-cancellation==0.2.1", "preprocess-cancellation==0.2.1",
"jinja2==3.1.5", "jinja2==3.1.6",
"dbus-fast>=2.21.3, <=2.44.1", "dbus-fast>=2.21.3, <=3.1.2",
"apprise==1.9.2", "apprise>=1.9.3, <=1.9.6",
"ldap3==2.9.1", "ldap3==2.9.1",
"python-periphery==2.4.1", "python-periphery==2.4.1",
"importlib_metadata==6.7.0 ; python_version=='3.7'", "importlib_metadata>=6.7.0, <=8.7.1"
"importlib_metadata==8.2.0 ; python_version>='3.8'"
] ]
requires-python = ">=3.7" requires-python = ">=3.7"
readme = "README.md" readme = "README.md"
@ -38,6 +37,8 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14"
] ]
[project.urls] [project.urls]
@ -53,7 +54,7 @@ speedups = [
"msgspec>=0.18.4 ; python_version>='3.8'", "msgspec>=0.18.4 ; python_version>='3.8'",
"uvloop>=0.17.0" "uvloop>=0.17.0"
] ]
dev = ["pre-commit"] dev = ["pre-commit", "build", "mypy", "ruff", "twine"]
[tool.pdm.version] [tool.pdm.version]
source = "scm" source = "scm"

View file

@ -12,6 +12,8 @@ LOG_PATH="${MOONRAKER_LOG_PATH}"
DATA_PATH="${MOONRAKER_DATA_PATH}" DATA_PATH="${MOONRAKER_DATA_PATH}"
INSTANCE_ALIAS="${MOONRAKER_ALIAS:-moonraker}" INSTANCE_ALIAS="${MOONRAKER_ALIAS:-moonraker}"
SPEEDUPS="${MOONRAKER_SPEEDUPS:-n}" SPEEDUPS="${MOONRAKER_SPEEDUPS:-n}"
DEV_INSTALL="${MOONRAKER_DEV_INSTALL:-n}"
PY_INST_TYPE="${MOONRAKER_PYTHON_INSTALL_TYPE:-venv}"
SERVICE_VERSION="1" SERVICE_VERSION="1"
DISTRIBUTION="" DISTRIBUTION=""
DISTRO_VERSION="" DISTRO_VERSION=""
@ -25,6 +27,14 @@ if [ ! -z "${MOONRAKER_FORCE_DEFAULTS}" ]; then
FORCE_SYSTEM_INSTALL=$MOONRAKER_FORCE_DEFAULTS FORCE_SYSTEM_INSTALL=$MOONRAKER_FORCE_DEFAULTS
fi fi
# Check if this is a dev container, apply dev defaults if not set by environment
if [ "${MOONRAKER_VENDOR}" = "vscode-dev" ]; then
echo "VSCode Dev Container detected..."
[ -z "${MOONRAKER_DEV_INSTALL}" ] && DEV_INSTALL="y"
[ -z "${MOONRAKER_PYTHON_INSTALL_TYPE}" ] && PY_INST_TYPE="user"
[ -z "${MOONRAKER_DISABLE_SYSTEMCTL}" ] && DISABLE_SYSTEMCTL="y"
fi
# Force script to exit if an error occurs # Force script to exit if an error occurs
set -e set -e
@ -34,6 +44,7 @@ SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
# Determine if Moonraker is to be installed from source # Determine if Moonraker is to be installed from source
if [ -f "${SRCDIR}/moonraker/__init__.py" ]; then if [ -f "${SRCDIR}/moonraker/__init__.py" ]; then
echo "Installing from Moonraker source..." echo "Installing from Moonraker source..."
cd $SRCDIR
IS_SRC_DIST="y" IS_SRC_DIST="y"
fi fi
@ -48,6 +59,7 @@ detect_distribution() {
# *** AUTO GENERATED OS PACKAGE SCRIPT START *** # *** AUTO GENERATED OS PACKAGE SCRIPT START ***
get_pkgs_script=$(cat << EOF get_pkgs_script=$(cat << EOF
from __future__ import annotations from __future__ import annotations
import os
import shlex import shlex
import re import re
import pathlib import pathlib
@ -101,15 +113,22 @@ class SysDepsParser:
version = distro_info.get("distro_version") version = distro_info.get("distro_version")
if version: if version:
self.distro_version = _convert_version(version) self.distro_version = _convert_version(version)
self.vendor: str = "" self.vendor: str = os.getenv("MOONRAKER_VENDOR", "")
if pathlib.Path("/etc/rpi-issue").is_file(): if not self.vendor and pathlib.Path("/etc/rpi-issue").is_file():
self.vendor = "raspberry-pi" self.vendor = "raspberry-pi"
exclusions = os.getenv("MOONRAKER_EXCLUDED_PKGS", "")
self.exclusions: List[str] = [
excl.strip() for excl in exclusions.split() if excl.strip()
]
def _parse_spec(self, full_spec: str) -> str | None: def _parse_spec(self, full_spec: str) -> str | None:
parts = full_spec.split(";", maxsplit=1) parts = full_spec.split(";", maxsplit=1)
if len(parts) == 1:
return full_spec
pkg_name = parts[0].strip() pkg_name = parts[0].strip()
if pkg_name in self.exclusions or not pkg_name:
logging.info(f"Package '{full_spec}' excluded by environment")
return None
if len(parts) == 1:
return pkg_name
expressions = re.split(r"( and | or )", parts[1].strip()) expressions = re.split(r"( and | or )", parts[1].strip())
if not len(expressions) & 1: if not len(expressions) & 1:
logging.info( logging.info(
@ -137,7 +156,7 @@ class SysDepsParser:
continue continue
elif last_logical_op is None: elif last_logical_op is None:
logging.info( logging.info(
f"Requirement specifier contains two seqential expressions " f"Requirement specifier contains two sequential expressions "
f"without a logical operator: {full_spec}") f"without a logical operator: {full_spec}")
return None return None
dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip()) dep_parts = re.split(r"(==|!=|<=|>=|<|>)", exp.strip())
@ -270,38 +289,52 @@ install_packages()
# Step 4: Create python virtual environment # Step 4: Create python virtual environment
create_virtualenv() create_virtualenv()
{ {
report_status "Installing python virtual environment..." if [ $PY_INST_TYPE = "system" ]; then
pip_inst="pip install"
elif [ $PY_INST_TYPE = "user" ]; then
pip_inst="pip install --user"
else
pip_inst="${PYTHONDIR}/bin/pip install"
report_status "Installing python virtual environment..."
# If venv exists and user prompts a rebuild, then do so # If venv exists and user prompts a rebuild, then do so
if [ -d ${PYTHONDIR} ] && [ $REBUILD_ENV = "y" ]; then if [ -d ${PYTHONDIR} ] && [ $REBUILD_ENV = "y" ]; then
report_status "Removing old virtualenv" report_status "Removing old virtualenv"
rm -rf ${PYTHONDIR} rm -rf ${PYTHONDIR}
fi
if [ ! -d ${PYTHONDIR} ]; then
virtualenv -p python3 ${PYTHONDIR}
#GET_PIP="${HOME}/get-pip.py"
#curl https://bootstrap.pypa.io/pip/3.6/get-pip.py -o ${GET_PIP}
#${PYTHONDIR}/bin/python ${GET_PIP}
#rm ${GET_PIP}
fi
fi fi
echo "Using pip install command '${pip_inst}'..."
if [ ! -d ${PYTHONDIR} ]; then
virtualenv -p /usr/bin/python3 ${PYTHONDIR}
#GET_PIP="${HOME}/get-pip.py"
#curl https://bootstrap.pypa.io/pip/3.6/get-pip.py -o ${GET_PIP}
#${PYTHONDIR}/bin/python ${GET_PIP}
#rm ${GET_PIP}
fi
# Install/update dependencies # Install/update dependencies
export SKIP_CYTHON=1 export SKIP_CYTHON=1
if [ $IS_SRC_DIST = "y" ]; then if [ $IS_SRC_DIST = "y" ]; then
report_status "Installing Moonraker python dependencies..." report_status "Installing Moonraker python dependencies..."
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/moonraker-requirements.txt $pip_inst -r ${SRCDIR}/scripts/moonraker-requirements.txt
if [ ${SPEEDUPS} = "y" ]; then if [ $DEV_INSTALL = "y" ]; then
report_status "Installing dev requirements..."
$pip_inst -r ${SRCDIR}/scripts/moonraker-speedups.txt
$pip_inst -r ${SRCDIR}/scripts/moonraker-dev-reqs.txt
$pip_inst -r ${SRCDIR}/docs/doc-requirements.txt
elif [ $SPEEDUPS = "y" ]; then
report_status "Installing Speedups..." report_status "Installing Speedups..."
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/moonraker-speedups.txt $pip_inst -r ${SRCDIR}/scripts/moonraker-speedups.txt
fi fi
else else
report_status "Installing Moonraker package via Pip..." report_status "Installing Moonraker package via Pip..."
if [ ${SPEEDUPS} = "y" ]; then if [ $DEV_INSTALL = "y" ]; then
${PYTHONDIR}/bin/pip install -U moonraker[speedups] $pip_inst -U moonraker[speedups,dev]
elif [ $SPEEDUPS = "y" ]; then
$pip_inst -U moonraker[speedups]
else else
${PYTHONDIR}/bin/pip install -U moonraker $pip_inst -U moonraker
fi fi
fi fi
} }
@ -349,9 +382,13 @@ EOF
# Step 6: Install startup script # Step 6: Install startup script
install_script() install_script()
{ {
if [ ! -d $SYSTEMDDIR ]; then
report_status "Systemd not detected, aborting service installation"
fi
# Create systemd service file # Create systemd service file
ENV_FILE="${DATA_PATH}/systemd/moonraker.env" ENV_FILE="${DATA_PATH}/systemd/moonraker.env"
if [ ! -f $ENV_FILE ] || [ $FORCE_SYSTEM_INSTALL = "y" ]; then if [ ! -f $ENV_FILE ] || [ $FORCE_SYSTEM_INSTALL = "y" ]; then
report_status "Creating systemd environment file ${ENV_FILE}..."
rm -f $ENV_FILE rm -f $ENV_FILE
env_vars="MOONRAKER_DATA_PATH=\"${DATA_PATH}\"" env_vars="MOONRAKER_DATA_PATH=\"${DATA_PATH}\""
[ -n "${CONFIG_PATH}" ] && env_vars="${env_vars}\nMOONRAKER_CONFIG_PATH=\"${CONFIG_PATH}\"" [ -n "${CONFIG_PATH}" ] && env_vars="${env_vars}\nMOONRAKER_CONFIG_PATH=\"${CONFIG_PATH}\""
@ -361,7 +398,9 @@ install_script()
echo -e $env_vars > $ENV_FILE echo -e $env_vars > $ENV_FILE
fi fi
[ -f $SERVICE_FILE ] && [ $FORCE_SYSTEM_INSTALL = "n" ] && return [ -f $SERVICE_FILE ] && [ $FORCE_SYSTEM_INSTALL = "n" ] && return
report_status "Installing system start script..." report_status "Installing systemd service unit..."
python_bin="${PYTHONDIR}/bin/python"
[ $PY_INST_TYPE != "venv" ] && python_bin="python3"
sudo groupadd -f moonraker-admin sudo groupadd -f moonraker-admin
sudo /bin/sh -c "cat > ${SERVICE_FILE}" << EOF sudo /bin/sh -c "cat > ${SERVICE_FILE}" << EOF
# systemd service file for moonraker # systemd service file for moonraker
@ -379,7 +418,7 @@ User=$USER
SupplementaryGroups=moonraker-admin SupplementaryGroups=moonraker-admin
RemainAfterExit=yes RemainAfterExit=yes
EnvironmentFile=${ENV_FILE} EnvironmentFile=${ENV_FILE}
ExecStart=${PYTHONDIR}/bin/python \$MOONRAKER_ARGS ExecStart=${python_bin} \$MOONRAKER_ARGS
Restart=always Restart=always
RestartSec=10 RestartSec=10
EOF EOF

View file

@ -1 +1,5 @@
pre-commit pre-commit
build
mypy
ruff
twine

View file

@ -1,19 +1,18 @@
# Python dependencies for Moonraker # Python dependencies for Moonraker
--find-links=python_wheels --find-links=python_wheels
tornado>=6.2.0, <=6.5.1 tornado>=6.2.0, <=6.5.4
pyserial==3.4 pyserial==3.4
pillow>=9.5.0, <=11.1.0 pillow>=9.5.0, <=12.1.0
streaming-form-data>=1.11.0, <=1.19.1 streaming-form-data>=1.11.0, <=1.19.1
distro==1.9.0 distro==1.9.0
inotify-simple==1.3.5 inotify-simple==2.0.1
libnacl==2.1.0 libnacl==2.1.0
paho-mqtt==1.6.1 paho-mqtt==2.1.0
zeroconf==0.131.0 zeroconf>=0.131.0, <=0.148.0
preprocess-cancellation==0.2.1 preprocess-cancellation==0.2.1
jinja2==3.1.5 jinja2==3.1.6
dbus-fast>=2.21.3, <=2.44.1 dbus-fast>=2.21.3, <=3.1.2
apprise==1.9.2 apprise>=1.9.3, <=1.9.6
ldap3==2.9.1 ldap3==2.9.1
python-periphery==2.4.1 python-periphery==2.4.1
importlib_metadata==6.7.0 ; python_version=='3.7' importlib_metadata>=6.7.0, <=8.7.1
importlib_metadata==8.2.0 ; python_version>='3.8'