Compare commits

...

10 Commits

Author SHA1 Message Date
Eric Callahan 30ac5dfae9
docs: update changelog
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-08-09 05:58:35 -04:00
Eric Callahan 0a8590643f
server: refactor startup
Use asyncio.run() to launch the server application as recommended
by the Python docs.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-08-08 16:29:01 -04:00
Eric Callahan 3a42dac02b announcements: use data path for dev mode
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-08-08 15:02:23 -04:00
Eric Callahan 863d0c1e4b confighelper: don't resolve backup path
If the configuration file is a symbolic link we want the backup
generated in the config folder.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-08-08 15:02:13 -04:00
Eric Callahan b40751ba9d docs: update install documentation
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-30 14:01:49 -04:00
Eric Callahan c2409e813e build: shared data fix
Make sure the scripts directory is included in the "data/share"
folder in the wheel.  In addition, remove the stray .gitignore
that is included when building a wheel from an sdist.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-30 14:01:49 -04:00
Eric Callahan 83371cf135 project: add support for dependency syncing
Use pre-commit to synchronize python requirements between
pyproject.toml and the various requirements files.  In addition,
syncronize the contents of system-dependencies.json with
Moonraker's installer.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-30 14:01:49 -04:00
Eric Callahan 247640cc27 install: add support for installing from pip
Include distribution  detection for future support of
distros not compatible with Debian.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-30 14:01:49 -04:00
Eric Callahan 63578aefd0
release: bump version to 0.9.1
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-25 15:02:46 -04:00
Eric Callahan 8debbf8ba4
source_info: resolve importlib_metadata compatibility issues
Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
2024-07-25 14:16:42 -04:00
19 changed files with 616 additions and 254 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ start_moonraker
.pdm-python
build
dist
share

13
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,13 @@
repos:
- repo: local
hooks:
- id: sync-requirements
name: sync python requirements
language: system
entry: python3 scripts/sync_dependencies.py
files: ^pyproject.toml$
- id: sync-os-packages
name: sync packages
language: system
entry: python3 scripts/sync_dependencies.py
files: ^scripts/system-dependencies.json$

View File

@ -6,6 +6,38 @@ The format is based on [Keep a Changelog].
## [Unreleased]
### Changed
- **server**: Use `asyncio.run` to launch the server as recommended by the
official Python documentation.
- **announcements**: Look for xml files at `<data_path>/development/announcements`
when `dev_mode` is set to True.
### Fixed
- **confighelper**: Don't resolve symbolic links to the main configuration file.
## [0.9.2] - 2024-07-30
### Added
- **install**: Add support for installing Moonraker's python package via pip.
- **scripts**: Add script to sync python and system dependencies from
`pyproject.toml` and `system-dependencies.json` respectively.
- **dev**: Add pre-commit hook to call `sync_dependencies.py`.
### Fixed
- **build**: Build from sdist now correctly includes share data.
- **build**: Remove stray `.gitignore` from Python Wheel.
### Changed
- **install**: The `MOONRAKER_FORCE_DEFAULTS` environment variable has changed
to `MOONRAKER_FORCE_SYSTEM_INSTALL`.
## [0.9.1] - 2024-07-25
### Fixed
- **source_info**: Fixed `importlib.metadata` compatibility issues with python
versions 3.9 or older.
## [0.9.0] - 2024-07-25
### Added
@ -184,7 +216,9 @@ The format is based on [Keep a Changelog].
[api_changes.md]: api_changes.md
<!-- Versions -->
[unreleased]: https://github.com/Arksine/moonraker/compare/v0.9.0...HEAD
[unreleased]: https://github.com/Arksine/moonraker/compare/v0.9.2...HEAD
[0.9.2]: https://github.com/Arksine/moonraker/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/Arksine/moonraker/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/Arksine/moonraker/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/Arksine/moonraker/compare/v0.7.1...v0.8.0
[0.7.1]: https://github.com/Arksine/moonraker/releases/tag/v0.7.1

View File

@ -2222,7 +2222,7 @@ folder that provides supplemental information for the application. The
Moonraker uses the [PDM backend](https://backend.pdm-project.org/) to perform
its package builds. An example of a pdm build script that generates a
`release_info` file may be found
[here](https://github.com/Arksine/moonraker/blob/master/scripts/pdm_build_dist.py).
[here](https://github.com/Arksine/moonraker/blob/master/pdm_build.py).
#### The System Dependencies File Format

View File

@ -1,19 +1,19 @@
## Installation
This document provides a guide on how to install Moonraker on a Raspberry
Pi running Raspian/Rasperry Pi OS. Other SBCs and/or linux distributions
may work, however they may need a custom install script. Moonraker
requires Python 3.7 or greater, verify that your distribution's
Python 3 packages meet this requirement.
This document provides a guide on how to install Moonraker on a Debian
based Linux Distributions. Other linux distributions may work, however
they may need a custom install script. Moonraker requires Python 3.7 or
greater, verify that your distribution's Python 3 packages meet this
requirement.
### Installing Klipper
Klipper should be installed prior to installing Moonraker. Please see
[Klipper's Documention](https://klipper3d.com/Overview.html) for details.
[Klipper's Documentation](https://klipper3d.com/Overview.html) for details.
After installing Klipper you should make sure to add Moonraker's
[configuration requirements](#klipper-configuration-requirements).
### Klipper Configuration Requirements
#### Klipper Configuration Requirements
Moonraker depends on the following Klippy extras for full functionality:
@ -34,7 +34,7 @@ missing one or both, you can simply add the bare sections to `printer.cfg`:
path: ~/printer_data/gcodes
```
### Enabling Klipper's Unix Domain Socket Server
#### Enabling Klipper's Unix Domain Socket Server
After Klipper is installed it may be necessary to modify its `defaults` file in
order to enable the Unix Domain Socket. Begin by opening the file in your
@ -53,7 +53,7 @@ KLIPPY_EXEC=/home/pi/klippy-env/bin/python
KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer.cfg -l /tmp/klippy.log"
```
Add `-a /tmp/klippy_uds` to KLIPPY_ARGS:
Add `-a /home/pi/printer_data/comms/klippy.sock` to KLIPPY_ARGS:
```
# Configuration for /etc/init.d/klipper
@ -61,7 +61,7 @@ KLIPPY_USER=pi
KLIPPY_EXEC=/home/pi/klippy-env/bin/python
KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer.cfg -l /tmp/klippy.log -a /tmp/klippy_uds"
KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer.cfg -l /tmp/klippy.log -a /home/pi/printer_data/comms/klippy.sock
```
!!! note
@ -79,7 +79,7 @@ KLIPPY_USER=pi
KLIPPY_EXEC=/home/pi/klippy-env/bin/python
KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer_data/config/printer.cfg -l /home/pi/printer_data/logs/klippy.log -a /tmp/klippy_uds"
KLIPPY_ARGS="/home/pi/klipper/klippy/klippy.py /home/pi/printer_data/config/printer.cfg -l /home/pi/printer_data/logs/klippy.log -a /home/pi/printer_data/comms/klippy.sock"
```
Moonraker's install script will create the data folder, however you
@ -94,13 +94,52 @@ mv printer.cfg ~/printer_data/config
### Installing Moonraker
Begin by cloning the git respository:
Moonraker provides an install script that can be used to facilitate
installation. The type of installation depends on where the install
script is located on the host file system. If the install script is
downloaded and run individually, the script will install Moonraker as
a Python Package using pip. If the script is run from Moonraker's
original source it will install Moonraker from source.
Prior to installation it is necessary to open a terminal on the host
machine, or SSH into it. It is recommended to read this entire
section before proceeding with the installation.
#### Installing the Moonraker Python package
The Python Package version of Moonraker will receive fewer updates, and
should generally be more stable. This is intended for users that do
not need to run unofficial 3rd party extensions, such as
[Moonraker-Timelapse](https://github.com/mainsail-crew/moonraker-timelapse),
and do not desire to run the "bleeding edge" version of Moonraker.
To perform this installation, download Moonraker's
[install script](https://raw.githubusercontent.com/Arksine/moonraker/master/scripts/install-moonraker.sh)
from Github, then run the installer:
```
cd ~
wget https://raw.githubusercontent.com/Arksine/moonraker/master/scripts/install-moonraker.sh
./install-moonraker.sh
```
#### Installing Moonraker from source
Moonraker can be run directly from source. This method of installation will
clone Moonraker's git repository, and may receive frequent bleeding edge
updates. Users who want to test Moonraker, run unofficial modifications, and
do not mind SSHing into the host to correct issues should choose this option.
To install Moonraker from source, clone its git repository then run the installer:
```
cd ~
git clone https://github.com/Arksine/moonraker.git
~/moonraker/scripts/install-moonraker.sh
```
#### Customizing the installation
The install script will attempt to create a basic configuration if
`moonraker.conf` does not exist at the expected location, however if you
prefer to have Moonraker start with a robust configuration you may create
@ -109,20 +148,17 @@ it now. By default the configuration file should be located at
data path may be configured using the script's command line options.
The [sample moonraker.conf](./moonraker.conf) may be used as a starting
point, full details can be found in the
[confguration documentation](./configuration.md).
[configuration documentation](./configuration.md).
For a default installation run the following commands:
```
cd ~/moonraker/scripts
./install-moonraker.sh
```
The install script has a few command line options that may be useful,
The install script has several command line options that may be useful,
particularly for those upgrading:
- `-f`:
Force an overwrite of Moonraker's systemd script. By default the
the systemd script will not be modified if it exists.
Force an overwrite of Moonraker's systemd script. In addition, a
new `moonraker.env` file will be created, and the PolKit rules will
be re-installed. By default these items will not be modified if
they exist.
- `-a <alias>`:
The installer uses this option to determine the name of the service
to install. If `-d` is not provided then this options will also be
@ -133,7 +169,7 @@ particularly for those upgrading:
files and directories used by moonraker. See the `Data Folder Structure`
section for details. If omitted this defaults to `$HOME/printer_data`.
- `-c <path to configuration file>`
Specifies the path to Moonraker's configuation file. By default the
Specifies the path to Moonraker's configuration file. By default the
configuration is expected at `<data_folder>/config/moonraker.conf`. ie:
`/home/pi/printer_data/config/moonraker.conf`.
- `-l <path to log file>`
@ -157,25 +193,26 @@ variables:
- `MOONRAKER_VENV`
- `MOONRAKER_REBUILD_ENV`
- `MOONRAKER_FORCE_DEFAULTS`
- `MOONRAKER_FORCE_SYSTEM_INSTALL`
- `MOONRAKER_DISABLE_SYSTEMCTL`
- `MOONRAKER_SKIP_POLKIT`
- `MOONRAKER_CONFIG_PATH`
- `MOONAKER_LOG_PATH`
- `MOONRAKER_LOG_PATH`
- `MOONRAKER_DATA_PATH`
- `MOONRAKER_SPEEDUPS`
When the script completes it should start both Moonraker and Klipper. In
`klippy.log` you should find the following entry:
When the script completes it should start the Moonraker system service. If Klipper
is running and Moonraker is able to establish a connection the following log entry
should be available in `klippy.log`:
`webhooks client <uid>: Client info {'program': 'Moonraker', 'version': '<version>'}`
Now you may install a client, such as
Now you may wish to install a frontend, such as
[Mainsail](https://github.com/mainsail-crew/mainsail) or
[Fluidd](https://github.com/fluidd-core/fluidd).
!!! Note
Moonraker's install script no longer includes the nginx dependency.
Moonraker's installer does not include nginx as a dependency.
If you want to install one of the above clients on the local machine,
you may want to first install nginx (`sudo apt install nginx` on
debian/ubuntu distros).
@ -299,7 +336,7 @@ Following are some items to take note of:
- The `EnvironmentFile` field contains Moonraker's arguments. See the
[environment file section](#the-environment-file) for details.
- The `ExecStart` field begins with the python executable, followed by
by the enviroment variable `MOONRAKER_ARGS`. This variable is set in
by the environment variable `MOONRAKER_ARGS`. This variable is set in
the environment file.
@ -362,7 +399,7 @@ If is necessary to run Moonraker without logging to a file the
While moonraker will still log to stdout, all requests for support
must be accompanied by `moonraker.log`.
Each command line argument has an associated enviroment variable that may
Each command line argument has an associated environment variable that may
be used to specify options in place of the command line.
- `MOONRAKER_DATA_PATH="<data path>"`: equivalent to `-d <data path>`
@ -460,7 +497,7 @@ with the `sudo` prefix. This has significant downsides:
wants to poll information about the system.
Moonraker now supports communicating with system services via D-Bus.
Operations that require elevated privileges are authrorized through
Operations that require elevated privileges are authorized through
PolicyKit. On startup Moonraker will check for the necessary privileges
and warn users if they are not available. Warnings are presented in
`moonraker.log` and directly to the user through some clients.

View File

@ -422,9 +422,13 @@ class RssFeed:
self.etag: Optional[str] = None
self.dev_xml_path: Optional[pathlib.Path] = None
if dev_mode:
res_dir = pathlib.Path(__file__).parent.parent.parent.resolve()
res_path = res_dir.joinpath(".devel/announcement_xml")
self.dev_xml_path = res_path.joinpath(self.xml_file)
data_path = pathlib.Path(self.server.get_app_arg("data_path"))
dev_folder = data_path.joinpath("development/announcements")
dev_folder.mkdir(parents=True, exist_ok=True)
self.dev_xml_path = dev_folder.joinpath(self.xml_file)
logging.info(
f"Announcement Feed {name}: Dev XML path set to {self.dev_xml_path}"
)
async def initialize(self) -> None:
self.etag = await self.database.get_item(

View File

@ -531,7 +531,7 @@ class ConfigHelper:
def create_backup(self) -> None:
cfg_path = self.server.get_app_args()["config_file"]
cfg = pathlib.Path(cfg_path).expanduser().resolve()
cfg = pathlib.Path(cfg_path).expanduser()
backup = cfg.parent.joinpath(f".{cfg.name}.bkp")
backup_fp: Optional[TextIO] = None
try:
@ -1108,7 +1108,10 @@ class FileSourceWrapper(ConfigSourceWrapper):
def get_configuration(
server: Server, app_args: Dict[str, Any]
) -> ConfigHelper:
start_path = pathlib.Path(app_args['config_file']).expanduser().resolve()
cfg_file = app_args["config_file"]
if app_args["is_backup_config"]:
cfg_file = app_args["backup_config"]
start_path = pathlib.Path(cfg_file).expanduser().absolute()
source = FileSourceWrapper(server)
source.read_file(start_path)
if not source.config.has_section('server'):
@ -1116,7 +1119,7 @@ def get_configuration(
return ConfigHelper(server, source, 'server', {})
def find_config_backup(cfg_path: str) -> Optional[str]:
cfg = pathlib.Path(cfg_path).expanduser().resolve()
cfg = pathlib.Path(cfg_path).expanduser()
backup = cfg.parent.joinpath(f".{cfg.name}.bkp")
if backup.is_file():
return str(backup)

View File

@ -27,7 +27,7 @@ _uvl_var = os.getenv("MOONRAKER_ENABLE_UVLOOP", "y").lower()
_uvl_enabled = False
if _uvl_var in ["y", "yes", "true"]:
with contextlib.suppress(ImportError):
import uvloop
import uvloop # type: ignore
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
_uvl_enabled = True
@ -48,7 +48,7 @@ class EventLoop:
return self.aioloop
def reset(self) -> None:
self.aioloop = self._create_new_loop()
self.aioloop = asyncio.get_running_loop()
self.add_signal_handler = self.aioloop.add_signal_handler
self.remove_signal_handler = self.aioloop.remove_signal_handler
self.add_reader = self.aioloop.add_reader
@ -126,7 +126,7 @@ class EventLoop:
host, port, family=0, type=socket.SOCK_STREAM
)
for res in ainfo:
af, socktype, proto, canonname, sa = res
af, socktype, proto, _cannon_name, _sa = res
sock = None
try:
sock = socket.socket(af, socktype, proto)
@ -152,12 +152,6 @@ class EventLoop:
else:
raise socket.error("getaddrinfo returns an empty list")
def start(self):
self.aioloop.run_forever()
def stop(self):
self.aioloop.stop()
def close(self):
self.aioloop.close()

View File

@ -130,6 +130,7 @@ class LogManager:
return eventloop.run_in_thread(self.file_hdlr.doRollover)
def stop_logging(self):
if self.listener is not None:
self.listener.stop()
async def _handle_log_rollover(

View File

@ -89,6 +89,7 @@ class Server:
self.ssl_port: int = config.getint('ssl_port', 7130)
self.exit_reason: str = ""
self.server_running: bool = False
self.app_running_evt = asyncio.Event()
self.pip_recovery_attempted: bool = False
# Configure Debug Logging
@ -198,10 +199,8 @@ class Server:
await self.event_loop.run_in_thread(self.config.create_backup)
machine: Machine = self.lookup_component("machine")
if await machine.validate_installation():
return
if start_server:
restarting = await machine.validate_installation()
if not restarting and start_server:
await self.start_server()
async def start_server(self, connect_to_klippy: bool = True) -> None:
@ -218,6 +217,9 @@ class Server:
if connect_to_klippy:
self.klippy_connection.connect()
async def run_until_exit(self) -> None:
await self.app_running_evt.wait()
def add_log_rollover_item(
self, name: str, item: str, log: bool = True
) -> None:
@ -488,7 +490,7 @@ class Server:
self.exit_reason = exit_reason
self.event_loop.remove_signal_handler(signal.SIGTERM)
self.event_loop.stop()
self.app_running_evt.set()
async def _handle_server_restart(self, web_request: WebRequest) -> str:
self.event_loop.register_callback(self._stop_server)
@ -540,6 +542,45 @@ class Server:
'files': cfg_file_list
}
async def launch_server(
log_manager: LogManager, app_args: Dict[str, Any]
) -> Optional[int]:
eventloop = EventLoop()
startup_warnings: List[str] = app_args["startup_warnings"]
try:
server = Server(app_args, log_manager, eventloop)
server.load_components()
except confighelper.ConfigError as e:
logging.exception("Server Config Error")
backup_cfg: Optional[str] = app_args["backup_config"]
if app_args["is_backup_config"] or backup_cfg is None:
return 1
app_args["is_backup_config"] = True
startup_warnings.append(
f"Server configuration error: {e}\n"
f"Loading most recent working configuration: '{backup_cfg}'\n"
f"Please fix the issue in moonraker.conf and restart the server."
)
return True
except Exception:
logging.exception("Moonraker Error")
return 1
try:
await server.server_init()
await server.run_until_exit()
except Exception:
logging.exception("Server Running Error")
return 1
if server.exit_reason == "terminate":
return 0
# Restore the original config and clear the warning
# before the server restarts
if app_args["is_backup_config"]:
startup_warnings.pop()
app_args["is_backup_config"] = False
del server
return None
def main(from_package: bool = True) -> None:
def get_env_bool(key: str) -> bool:
return os.getenv(key, "").lower() in ["y", "yes", "true"]
@ -635,6 +676,7 @@ def main(from_package: bool = True) -> None:
"data_path": str(data_path),
"is_default_data_path": cmd_line_args.datapath is None,
"config_file": cfg_file,
"backup_config": confighelper.find_config_backup(cfg_file),
"startup_warnings": startup_warnings,
"verbose": cmd_line_args.verbose,
"debug": cmd_line_args.debug,
@ -661,60 +703,14 @@ def main(from_package: bool = True) -> None:
log_manager = LogManager(app_args, startup_warnings)
# Start asyncio event loop and server
event_loop = EventLoop()
alt_config_loaded = False
estatus = 0
while True:
try:
server = Server(app_args, log_manager, event_loop)
server.load_components()
except confighelper.ConfigError as e:
backup_cfg = confighelper.find_config_backup(cfg_file)
logging.exception("Server Config Error")
if alt_config_loaded or backup_cfg is None:
estatus = 1
estatus = asyncio.run(launch_server(log_manager, app_args))
if estatus is not None:
break
app_args["config_file"] = backup_cfg
app_args["is_backup_config"] = True
warn_list = list(startup_warnings)
app_args["startup_warnings"] = warn_list
warn_list.append(
f"Server configuration error: {e}\n"
f"Loaded server from most recent working configuration:"
f" '{app_args['config_file']}'\n"
f"Please fix the issue in moonraker.conf and restart "
f"the server."
)
alt_config_loaded = True
continue
except Exception:
logging.exception("Moonraker Error")
estatus = 1
break
try:
event_loop.register_callback(server.server_init)
event_loop.start()
except Exception:
logging.exception("Server Running Error")
estatus = 1
break
if server.exit_reason == "terminate":
break
# Restore the original config and clear the warning
# before the server restarts
if alt_config_loaded:
app_args["config_file"] = cfg_file
app_args["startup_warnings"] = startup_warnings
app_args["is_backup_config"] = False
alt_config_loaded = False
event_loop.close()
# Since we are running outside of the the server
# it is ok to use a blocking sleep here
time.sleep(.5)
logging.info("Attempting Server Restart...")
del server
event_loop.reset()
event_loop.close()
logging.info("Server Shutdown")
log_manager.stop_logging()
exit(estatus)

View File

@ -13,10 +13,7 @@ import re
import json
import logging
from dataclasses import dataclass
if sys.version_info < (3, 8):
from importlib_metadata import Distribution, PathDistribution, PackageMetadata
else:
from importlib.metadata import Distribution, PathDistribution, PackageMetadata
from importlib_metadata import Distribution, PathDistribution, PackageMetadata
from .exceptions import ServerError
# Annotation imports

103
pdm_build.py Normal file
View File

@ -0,0 +1,103 @@
# Wheel Setup Script for generating metadata
#
# Copyright (C) 2023 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
from __future__ import annotations
import pathlib
import subprocess
import shlex
import json
import shutil
from datetime import datetime, timezone
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from pdm.backend.hooks.base import Context
__package_name__ = "moonraker"
__dependencies__ = "scripts/system-dependencies.json"
def _run_git_command(cmd: str) -> str:
prog = shlex.split(cmd)
process = subprocess.Popen(
prog, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
ret, err = process.communicate()
retcode = process.wait()
if retcode == 0:
return ret.strip().decode()
return ""
def get_commit_sha(source_path: pathlib.Path) -> str:
cmd = f"git -C {source_path} rev-parse HEAD"
return _run_git_command(cmd)
def retrieve_git_version(source_path: pathlib.Path) -> str:
cmd = f"git -C {source_path} describe --always --tags --long --dirty"
return _run_git_command(cmd)
def pdm_build_clean(context: Context) -> None:
share_path: pathlib.Path = context.root.joinpath("share")
if share_path.exists():
shutil.rmtree(str(share_path))
def pdm_build_initialize(context: Context) -> None:
context.ensure_build_dir()
proj_name: str = context.config.metadata['name']
build_dir = pathlib.Path(context.build_dir)
data_path = context.root.joinpath(f"share/{proj_name}")
pkg_path = build_dir.joinpath(__package_name__)
pkg_path.mkdir(parents=True, exist_ok=True)
rinfo_path: pathlib.Path = pkg_path.joinpath("release_info")
rinfo_data: str = ""
if context.root.joinpath(".git").exists():
build_ver: str = context.config.metadata['version']
build_time = datetime.now(timezone.utc)
urls: Dict[str, str] = context.config.metadata['urls']
release_info: Dict[str, Any] = {
"project_name": proj_name,
"package_name": __package_name__,
"urls": {key.lower(): val for key, val in urls.items()},
"package_version": build_ver,
"git_version": retrieve_git_version(context.root),
"commit_sha": get_commit_sha(context.root),
"build_time": datetime.isoformat(build_time, timespec="seconds")
}
if __dependencies__:
deps = pathlib.Path(context.root).joinpath(__dependencies__)
if deps.is_file():
dep_info: Dict[str, Any] = json.loads(deps.read_bytes())
release_info["system_dependencies"] = dep_info
# Write the release info to both the package and the data path
rinfo_data = json.dumps(release_info, indent=4)
rinfo_path.write_text(rinfo_data)
else:
rinfo_path = context.root.joinpath(f"{proj_name}/release_info")
if rinfo_path.is_file():
rinfo_data = rinfo_path.read_text()
else:
rinfo_data = ""
data_path.mkdir(parents=True, exist_ok=True)
if rinfo_data:
data_path.joinpath("release_info").write_text(rinfo_data)
scripts_path: pathlib.Path = context.root.joinpath("scripts")
scripts_dest: pathlib.Path = data_path.joinpath("scripts")
scripts_dest.mkdir()
for item in scripts_path.iterdir():
if item.name in ("__pycache__", "python_wheels"):
continue
if item.is_dir():
shutil.copytree(str(item), str(scripts_dest.joinpath(item.name)))
else:
shutil.copy2(str(item), str(scripts_dest))
git_ignore = build_dir.joinpath(".gitignore")
if git_ignore.is_file():
git_ignore.unlink()
def pdm_build_finalize(context: Context, artifact: pathlib.Path) -> None:
share_path: pathlib.Path = context.root.joinpath("share")
if share_path.exists():
shutil.rmtree(str(share_path))

View File

@ -25,7 +25,8 @@ dependencies = [
"apprise==1.8.0",
"ldap3==2.9.1",
"python-periphery==2.4.1",
"importlib_metadata==6.7.0 ; python_version=='3.7'"
"importlib_metadata==6.7.0 ; python_version=='3.7'",
"importlib_metadata==8.2.0 ; python_version>='3.8'"
]
requires-python = ">=3.7"
readme = "README.md"
@ -50,9 +51,13 @@ documentation = "https://moonraker.readthedocs.io"
changelog = "https://moonraker.readthedocs.io/en/latest/changelog/"
[project.optional-dependencies]
msgspec=["msgspec>=0.18.4 ; python_version>='3.8'"]
uvloop=["uvloop>=0.17.0"]
speedups = ["moonraker[msgspec,uvloop]"]
msgspec = ["msgspec>=0.18.4 ; python_version>='3.8'"]
uvloop = ["uvloop>=0.17.0"]
speedups = [
"msgspec>=0.18.4 ; python_version>='3.8'",
"uvloop>=0.17.0"
]
dev = ["pre-commit"]
[tool.pdm.version]
source = "scm"
@ -62,8 +67,12 @@ write_template = "__version__ = '{}'\n"
[tool.pdm.build]
excludes = ["./**/.git", "moonraker/moonraker.py"]
includes = ["moonraker"]
source-includes = ["scripts"]
editable-backend = "path"
custom-hook = "scripts/pdm_build_dist.py"
custom-hook = "pdm_build.py"
[tool.pdm.build.wheel-data]
data = [{path = "share/moonraker/**/*", relative-to = "."}]
[project.scripts]
moonraker = "moonraker.server:main"

View File

@ -1,11 +1,11 @@
#!/bin/bash
# This script installs Moonraker on a Raspberry Pi machine running
# Raspbian/Raspberry Pi OS based distributions.
# This script installs Moonraker on Debian based Linux distros.
SUPPORTED_DISTROS="debian"
PYTHONDIR="${MOONRAKER_VENV:-${HOME}/moonraker-env}"
SYSTEMDDIR="/etc/systemd/system"
REBUILD_ENV="${MOONRAKER_REBUILD_ENV:-n}"
FORCE_DEFAULTS="${MOONRAKER_FORCE_DEFAULTS:-n}"
FORCE_SYSTEM_INSTALL="${MOONRAKER_FORCE_SYSTEM_INSTALL:-n}"
DISABLE_SYSTEMCTL="${MOONRAKER_DISABLE_SYSTEMCTL:-n}"
SKIP_POLKIT="${MOONRAKER_SKIP_POLKIT:-n}"
CONFIG_PATH="${MOONRAKER_CONFIG_PATH}"
@ -14,17 +14,77 @@ DATA_PATH="${MOONRAKER_DATA_PATH}"
INSTANCE_ALIAS="${MOONRAKER_ALIAS:-moonraker}"
SPEEDUPS="${MOONRAKER_SPEEDUPS:-n}"
SERVICE_VERSION="1"
DISTRIBUTION=""
IS_SRC_DIST="n"
PACKAGES=""
package_decode_script=$( cat << EOF
import sys
import json
try:
ret = json.load(sys.stdin)
except Exception:
exit(0)
sys.stdout.write(' '.join(ret['debian']))
EOF
)
# Check deprecated FORCE_DEFAULTS environment variable
if [ ! -z "${MOONRAKER_FORCE_DEFAULTS}" ]; then
echo "Deprecated MOONRAKER_FORCE_DEFAULTS environment variable"
echo -e "detected. Please use MOONRAKER_FORCE_SYSTEM_INSTALL\n"
FORCE_SYSTEM_INSTALL=$MOONRAKER_FORCE_DEFAULTS
fi
# Force script to exit if an error occurs
set -e
# Find source director from the pathname of this script
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
# Determine if Moonraker is to be installed from source
if [ -f "${SRCDIR}/moonraker/__init__.py" ]; then
echo "Installing from Moonraker source..."
IS_SRC_DIST="y"
fi
# Detect Current Distribution
detect_distribution() {
distro_list=""
if [ -f "/etc/os-release" ]; then
distro_list="$( grep -Po "^ID=\K.+" /etc/os-release || true )"
like_str="$( grep -Po "^ID_LIKE=\K.+" /etc/os-release || true )"
if [ ! -z "${like_str}" ]; then
distro_list="${distro_list} ${like_str}"
fi
if [ ! -z "${distro_list}" ]; then
echo "Found Linux distribution IDs: ${distro_list}"
else
echo "Unable to detect Linux Distribution."
fi
fi
distro_id=""
while [ "$distro_list" != "$distro_id" ]; do
distro_id="${distro_list%% *}"
distro_list="${distro_list#$distro_id }"
supported_dists=$SUPPORTED_DISTROS
supported_id=""
while [ "$supported_dists" != "$supported_id" ]; do
supported_id="${supported_dists%% *}"
supported_dists="${supported_dists#$supported_id }"
if [ "$distro_id" = "$supported_id" ]; then
DISTRIBUTION=$distro_id
echo "Distribution detected: $DISTRIBUTION"
break
fi
done
[ ! -z "$DISTRIBUTION" ] && break
done
if [ -z "$DISTRIBUTION" ] && [ -x "$( which apt-get || true )" ]; then
# Fall back to debian if apt-get is deteted
echo "Found apt-get, falling back to debian distribution"
DISTRIBUTION="debian"
fi
# *** AUTO GENERATED OS PACKAGE DEPENDENCES START ***
if [ ${DISTRIBUTION} = "debian" ]; then
PACKAGES="python3-virtualenv python3-dev libopenjp2-7 libsodium-dev zlib1g-dev"
PACKAGES="${PACKAGES} libjpeg-dev packagekit wireless-tools curl"
PACKAGES="${PACKAGES} build-essential"
fi
# *** AUTO GENERATED OS PACKAGE DEPENDENCES END ***
}
# Step 2: Clean up legacy installation
cleanup_legacy() {
@ -41,30 +101,19 @@ cleanup_legacy() {
# Step 3: Install packages
install_packages()
{
if [ -z "${PACKAGES}" ]; then
echo "Unsupported Linux Distribution ${DISTRIBUTION}. "
echo "Bypassing system package installation."
return
fi
# Update system package info
report_status "Running apt-get update..."
sudo apt-get update --allow-releaseinfo-change
system_deps="${SRCDIR}/scripts/system-dependencies.json"
if [ -f "${system_deps}" ]; then
if [ ! -x "$(command -v python3)" ]; then
report_status "Installing python3 base package..."
sudo apt-get install --yes python3
fi
PKGS="$( cat ${system_deps} | python3 -c "${package_decode_script}" )"
else
echo "Error: system-dependencies.json not found, falling back to legacy pacakge list"
PKGLIST="${PKGLIST} python3-virtualenv python3-dev"
PKGLIST="${PKGLIST} libopenjp2-7 libsodium-dev zlib1g-dev libjpeg-dev"
PKGLIST="${PKGLIST} packagekit wireless-tools curl"
PKGS=${PKGLIST}
fi
# Install desired packages
report_status "Installing Moonraker Dependencies:"
report_status "${PKGS}"
sudo apt-get install --yes ${PKGS}
report_status "${PACKAGES}"
sudo apt-get install --yes ${PACKAGES}
}
# Step 4: Create python virtual environment
@ -88,12 +137,22 @@ create_virtualenv()
# Install/update dependencies
export SKIP_CYTHON=1
if [ $IS_SRC_DIST = "y" ]; then
report_status "Installing Moonraker python dependencies..."
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/moonraker-requirements.txt
if [ ${SPEEDUPS} = "y" ]; then
report_status "Installing Speedups..."
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/moonraker-speedups.txt
fi
else
report_status "Installing Moonraker package via Pip..."
if [ ${SPEEDUPS} = "y" ]; then
${PYTHONDIR}/bin/pip install -U moonraker[speedups]
else
${PYTHONDIR}/bin/pip install -U moonraker
fi
fi
}
# Step 5: Initialize data folder
@ -141,16 +200,16 @@ install_script()
{
# Create systemd service file
ENV_FILE="${DATA_PATH}/systemd/moonraker.env"
if [ ! -f $ENV_FILE ] || [ $FORCE_DEFAULTS = "y" ]; then
if [ ! -f $ENV_FILE ] || [ $FORCE_SYSTEM_INSTALL = "y" ]; then
rm -f $ENV_FILE
env_vars="MOONRAKER_DATA_PATH=\"${DATA_PATH}\""
[ -n "${CONFIG_PATH}" ] && env_vars="${env_vars}\nMOONRAKER_CONFIG_PATH=\"${CONFIG_PATH}\""
[ -n "${LOG_PATH}" ] && env_vars="${env_vars}\nMOONRAKER_LOG_PATH=\"${LOG_PATH}\""
env_vars="${env_vars}\nMOONRAKER_ARGS=\"-m moonraker\""
env_vars="${env_vars}\nPYTHONPATH=\"${SRCDIR}\"\n"
[ $IS_SRC_DIST = "y" ] && env_vars="${env_vars}\nPYTHONPATH=\"${SRCDIR}\"\n"
echo -e $env_vars > $ENV_FILE
fi
[ -f $SERVICE_FILE ] && [ $FORCE_DEFAULTS = "n" ] && return
[ -f $SERVICE_FILE ] && [ $FORCE_SYSTEM_INSTALL = "n" ] && return
report_status "Installing system start script..."
sudo groupadd -f moonraker-admin
sudo /bin/sh -c "cat > ${SERVICE_FILE}" << EOF
@ -184,32 +243,51 @@ EOF
check_polkit_rules()
{
if [ ! -x "$(command -v pkaction || true)" ]; then
echo "PolKit not installed"
return
fi
if [ "${SKIP_POLKIT}" = "y" ]; then
echo "Skipping PolKit rules installation"
return
fi
POLKIT_VERSION="$( pkaction --version | grep -Po "(\d+\.?\d*)" )"
NEED_POLKIT_INSTALL="n"
if [ $FORCE_SYSTEM_INSTALL = "n" ]; then
if [ "$POLKIT_VERSION" = "0.105" ]; then
POLKIT_LEGACY_FILE="/etc/polkit-1/localauthority/50-local.d/10-moonraker.pkla"
# legacy policykit rules don't give users other than root read access
if sudo [ ! -f $POLKIT_LEGACY_FILE ]; then
NEED_POLKIT_INSTALL="y"
else
echo "PolKit rules file found at ${POLKIT_LEGACY_FILE}"
fi
else
POLKIT_FILE="/etc/polkit-1/rules.d/moonraker.rules"
POLKIT_USR_FILE="/usr/share/polkit-1/rules.d/moonraker.rules"
if [ ! -f $POLKIT_FILE ] && [ ! -f $POLKIT_USR_FILE ]; then
if sudo [ -f $POLKIT_FILE ]; then
echo "PolKit rules file found at ${POLKIT_FILE}"
elif sudo [ -f $POLKIT_USR_FILE ]; then
echo "PolKit rules file found at ${POLKIT_USR_FILE}"
else
NEED_POLKIT_INSTALL="y"
fi
fi
if [ "${NEED_POLKIT_INSTALL}" = "y" ]; then
if [ "${SKIP_POLKIT}" = "y" ]; then
echo -e "\n*** No PolicyKit Rules detected, run 'set-policykit-rules.sh'"
echo "*** if you wish to grant Moonraker authorization to manage"
echo "*** system services, reboot/shutdown the system, and update"
echo "*** packages."
else
NEED_POLKIT_INSTALL="y"
fi
if [ "${NEED_POLKIT_INSTALL}" = "y" ]; then
report_status "Installing PolKit Rules"
${SRCDIR}/scripts/set-policykit-rules.sh -z
polkit_script="${SRCDIR}/scripts/set-policykit-rules.sh"
if [ $IS_SRC_DIST != "y" ]; then
polkit_script="${PYTHONDIR}/share/moonraker"
polkit_script="${polkit_script}/scripts/set-policykit-rules.sh"
fi
if [ -f "$polkit_script" ]; then
set +e
$polkit_script -z
set -e
else
echo "PolKit rule install script not found at $polkit_script"
fi
fi
}
@ -235,17 +313,11 @@ verify_ready()
fi
}
# Force script to exit if an error occurs
set -e
# Find SRCDIR from the pathname of this script
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
# Parse command line arguments
while getopts "rfzxsc:l:d:a:" arg; do
case $arg in
r) REBUILD_ENV="y";;
f) FORCE_DEFAULTS="y";;
f) FORCE_SYSTEM_INSTALL="y";;
z) DISABLE_SYSTEMCTL="y";;
x) SKIP_POLKIT="y";;
s) SPEEDUPS="y";;
@ -273,6 +345,7 @@ SERVICE_FILE="${SYSTEMDDIR}/${INSTANCE_ALIAS}.service"
# Run installation steps defined above
verify_ready
detect_distribution
cleanup_legacy
install_packages
create_virtualenv

View File

@ -0,0 +1 @@
pre-commit

View File

@ -20,3 +20,4 @@ apprise==1.8.0
ldap3==2.9.1
python-periphery==2.4.1
importlib_metadata==6.7.0 ; python_version=='3.7'
importlib_metadata==8.2.0 ; python_version>='3.8'

View File

@ -1,80 +0,0 @@
# Wheel Setup Script for generating metadata
#
# Copyright (C) 2023 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
from __future__ import annotations
import pathlib
import subprocess
import shlex
import json
import shutil
from datetime import datetime, timezone
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from pdm.backend.hooks.base import Context
__package_name__ = "moonraker"
__dependencies__ = "scripts/system-dependencies.json"
def _run_git_command(cmd: str) -> str:
prog = shlex.split(cmd)
process = subprocess.Popen(
prog, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
ret, err = process.communicate()
retcode = process.wait()
if retcode == 0:
return ret.strip().decode()
return ""
def get_commit_sha(source_path: pathlib.Path) -> str:
cmd = f"git -C {source_path} rev-parse HEAD"
return _run_git_command(cmd)
def retrieve_git_version(source_path: pathlib.Path) -> str:
cmd = f"git -C {source_path} describe --always --tags --long --dirty"
return _run_git_command(cmd)
def pdm_build_initialize(context: Context) -> None:
context.ensure_build_dir()
build_ver: str = context.config.metadata['version']
proj_name: str = context.config.metadata['name']
urls: Dict[str, str] = context.config.metadata['urls']
build_dir = pathlib.Path(context.build_dir)
rel_dpath = f"{__package_name__}-{build_ver}.data/data/share/{proj_name}"
data_path = build_dir.joinpath(rel_dpath)
pkg_path = build_dir.joinpath(__package_name__)
build_time = datetime.now(timezone.utc)
release_info: Dict[str, Any] = {
"project_name": proj_name,
"package_name": __package_name__,
"urls": {key.lower(): val for key, val in urls.items()},
"package_version": build_ver,
"git_version": retrieve_git_version(context.root),
"commit_sha": get_commit_sha(context.root),
"build_time": datetime.isoformat(build_time, timespec="seconds")
}
if __dependencies__:
deps = pathlib.Path(context.root).joinpath(__dependencies__)
if deps.is_file():
dep_info: Dict[str, Any] = json.loads(deps.read_bytes())
release_info["system_dependencies"] = dep_info
# Write the release info to both the package and the data path
rinfo_data = json.dumps(release_info, indent=4)
data_path.mkdir(parents=True, exist_ok=True)
pkg_path.mkdir(parents=True, exist_ok=True)
data_path.joinpath("release_info").write_text(rinfo_data)
pkg_path.joinpath("release_info").write_text(rinfo_data)
scripts_path = context.root.joinpath("scripts")
scripts_dest = data_path.joinpath("scripts")
scripts_dest.mkdir()
for item in scripts_path.iterdir():
if item.name == "__pycache__":
continue
if item.is_dir():
shutil.copytree(str(item), str(scripts_dest.joinpath(item.name)))
else:
shutil.copy2(str(item), str(scripts_dest))

174
scripts/sync_dependencies.py Executable file
View File

@ -0,0 +1,174 @@
#! /usr/bin/python3
# Script for syncing package dependencies and python reqs
#
# Copyright (C) 2024 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
from __future__ import annotations
import argparse
import pathlib
import tomllib
import json
import re
from typing import Dict, List
MAX_LINE_LENGTH = 88
SCRIPTS_PATH = pathlib.Path(__file__).parent
INST_PKG_HEADER = "# *** AUTO GENERATED OS PACKAGE DEPENDENCES START ***"
INST_PKG_FOOTER = "# *** AUTO GENERATED OS PACKAGE DEPENDENCES END ***"
def gen_multline_var(var_name: str, values: List[str], indent: int = 0) -> str:
idt = " " * indent
if not values:
return f'{idt}{var_name}=""'
line_list: List[str] = []
current_line = f"{idt}{var_name}=\"{values.pop(0)}"
for val in values:
if len(current_line) + len(val) + 2 > MAX_LINE_LENGTH:
line_list.append(f'{current_line}"')
current_line = (f"{idt}{var_name}=\"${{{var_name}}} {val}")
else:
current_line += f" {val}"
line_list.append(f'{current_line}"')
return "\n".join(line_list)
def sync_packages() -> int:
inst_script = SCRIPTS_PATH.joinpath("install-moonraker.sh")
sys_deps_file = SCRIPTS_PATH.joinpath("system-dependencies.json")
new_deps: Dict[str, List[str]] = json.loads(sys_deps_file.read_bytes())
# Copy install script in memory.
install_data: List[str] = []
prev_deps: Dict[str, List[str]] = {}
distro_name = ""
skip_data = False
with inst_script.open("r") as inst_file:
for line in inst_file:
cur_line = line.strip()
if not skip_data:
install_data.append(line)
else:
# parse current dependencies
distro_match = re.match(
r"(?:el)?if \[ \$\{DISTRIBUTION\} = \"([a-z0-9._-]+)\" \]; then",
cur_line
)
if distro_match is not None:
distro_name = distro_match.group(1)
prev_deps[distro_name] = []
elif cur_line.startswith("PACKAGES"):
pkgs = cur_line.split("=", maxsplit=1)[1].strip('"')
pkg_list = pkgs.split()
if pkg_list and pkg_list[0] == "${PACKAGES}":
pkg_list.pop(0)
prev_deps[distro_name].extend(pkg_list)
if cur_line == INST_PKG_HEADER:
skip_data = True
elif cur_line == INST_PKG_FOOTER:
skip_data = False
install_data.append(line)
# Check if an update is necessary
if set(prev_deps.keys()) == set(new_deps.keys()):
for distro, pkg_list in prev_deps.items():
new_pkgs = new_deps[distro]
if set(pkg_list) != set(new_pkgs):
break
else:
# Dependencies match, exit
print("System package dependencies match")
return 0
print("Writing new system dependencies to install script...")
with inst_script.open("w+") as inst_file:
# Find and replace old package defs
for line in install_data:
inst_file.write(line)
if line.strip() == INST_PKG_HEADER:
indent_count = len(line) - len(line.lstrip())
idt = " " * indent_count
# Write Package data
first_distro = True
for distro, packages in new_deps.items():
prefix = f"{idt}if" if first_distro else f"{idt}elif"
first_distro = False
inst_file.write(
f'{prefix} [ ${{DISTRIBUTION}} = "{distro}" ]; then\n'
)
pkg_var = gen_multline_var("PACKAGES", packages, indent_count + 4)
inst_file.write(pkg_var)
inst_file.write("\n")
inst_file.write(f"{idt}fi\n")
return 1
def check_reqs_changed(reqs_file: pathlib.Path, new_reqs: List[str]) -> bool:
req_list = []
for requirement in reqs_file.read_text().splitlines():
requirement = requirement.strip()
if not requirement or requirement[0] in ("-", "#"):
continue
req_list.append(requirement)
return set(new_reqs) != set(req_list)
def sync_requirements() -> int:
ret: int = 0
src_path = SCRIPTS_PATH.parent
proj_file = src_path.joinpath("pyproject.toml")
with proj_file.open("rb") as f:
data = tomllib.load(f)
python_deps = data["project"]["dependencies"]
optional_deps = data["project"]["optional-dependencies"]
reqs_path = SCRIPTS_PATH.joinpath("moonraker-requirements.txt")
if check_reqs_changed(reqs_path, python_deps):
print("Syncing Moonraker Python Requirements...")
ret = 1
with reqs_path.open("w+") as req_file:
req_file.write("# Python dependencies for Moonraker\n")
req_file.write("--find-links=python_wheels\n")
for requirement in python_deps:
req_file.write(f"{requirement}\n")
else:
print("Moonraker Python requirements match")
# sync speedups
speedups_path = SCRIPTS_PATH.joinpath("moonraker-speedups.txt")
speedup_deps = optional_deps["speedups"]
if check_reqs_changed(speedups_path, speedup_deps):
print("Syncing speedup requirements...")
ret = 1
with speedups_path.open("w+") as req_file:
for requirement in speedup_deps:
req_file.write(f"{requirement}\n")
else:
print("Speedup sequirements match")
# sync dev dependencies
dev_reqs_path = SCRIPTS_PATH.joinpath("moonraker-dev-reqs.txt")
dev_deps = optional_deps["dev"]
if check_reqs_changed(dev_reqs_path, dev_deps):
print("Syncing dev requirements")
ret = 1
with dev_reqs_path.open("r+") as req_file:
for requirement in dev_deps:
req_file.write(f"{requirement}\n")
else:
print("Dev requirements match")
return ret
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"filename", default="", nargs="?",
help="The name of the dependency file to sync"
)
args = parser.parse_args()
fname: str = args.filename
if not fname:
ret = sync_requirements()
ret += sync_packages()
return 1 if ret > 0 else 0
elif fname == "pyproject.toml":
return sync_requirements()
elif fname == "scripts/system-dependencies.json":
return sync_packages()
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -8,6 +8,7 @@
"libjpeg-dev",
"packagekit",
"wireless-tools",
"curl"
"curl",
"build-essential"
]
}