Add Nix package, NixOS module, and tests for bambuddy
- packages.bambuddy: FastAPI backend + prebuilt React frontend assembled into the app-root layout upstream expects, behind a single `bambuddy` launcher (uvicorn --loop asyncio, ffmpeg on PATH, XDG state defaults). Pins upstream maziggy/bambuddy v1.2.5.7. - nixosModules.default: services.bambuddy with host/port/dataDir, openFirewall, environment + environmentFile, extraPackages, DynamicUser + StateDirectory, systemd hardening, and CAP_NET_BIND_SERVICE for the virtual-printer FTPS/RTSP ports. - checks.bambuddy-import: backend import + admin CLI smoke test. - checks.bambuddy-service: two-node NixOS VM test covering the health endpoint, UI/static assets/manifest, SQLite + logs in the StateDirectory, journal startup check, cross-node reachability with openFirewall, custom host/port/env node, and loopback-only binding. - overlay, run app, devShell, README, .gitignore.
This commit is contained in:
commit
5fb8e8d724
9 changed files with 761 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
result
|
||||||
|
result-*
|
||||||
109
README.md
Normal file
109
README.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# bambuddy-nix
|
||||||
|
|
||||||
|
Nix packaging for [Bambuddy](https://github.com/maziggy/bambuddy) — a
|
||||||
|
self-hosted command center for Bambu Lab 3D printers (works offline via
|
||||||
|
Developer Mode, no cloud dependency).
|
||||||
|
|
||||||
|
This flake provides:
|
||||||
|
|
||||||
|
- **`packages.<system>.bambuddy`** — the app: FastAPI backend + prebuilt React
|
||||||
|
frontend behind a single `bambuddy` launcher
|
||||||
|
- **`nixosModules.bambuddy`** — a `services.bambuddy` NixOS module
|
||||||
|
- **checks** — an import smoke test and NixOS VM integration tests
|
||||||
|
|
||||||
|
## Quick start (NixOS)
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# flake.nix
|
||||||
|
{
|
||||||
|
inputs.bambuddy-nix.url = "github:<you>/bambuddy-nix";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, bambuddy-nix, ... }: {
|
||||||
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
bambuddy-nix.nixosModules.default
|
||||||
|
./configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
services.bambuddy = {
|
||||||
|
enable = true;
|
||||||
|
port = 8000; # web UI / API
|
||||||
|
openFirewall = true; # reachable from the LAN
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The dashboard is then available at `http://<host>:8000`. Add your printer(s)
|
||||||
|
through the first-run wizard — the printer must be on the same LAN with
|
||||||
|
Developer Mode enabled.
|
||||||
|
|
||||||
|
### Module options
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `services.bambuddy.enable` | `false` | Enable the service |
|
||||||
|
| `services.bambuddy.package` | flake's `bambuddy` | Package to run |
|
||||||
|
| `services.bambuddy.host` | `"0.0.0.0"` | Bind address (use `127.0.0.1` behind a reverse proxy) |
|
||||||
|
| `services.bambuddy.port` | `8000` | Web UI / API port |
|
||||||
|
| `services.bambuddy.dataDir` | `/var/lib/bambuddy` | SQLite DB, print archive, plate calibration, logs |
|
||||||
|
| `services.bambuddy.openFirewall` | `false` | Open the web port for LAN access |
|
||||||
|
| `services.bambuddy.environment` | `{}` | Extra env vars (see upstream `.env.example`) |
|
||||||
|
| `services.bambuddy.environmentFile` | `null` | Env file for secrets (`MFA_ENCRYPTION_KEY`, OIDC, …) |
|
||||||
|
| `services.bambuddy.extraPackages` | `[]` | Extra tools on the service PATH (e.g. `tailscale`) |
|
||||||
|
|
||||||
|
The service runs as a `DynamicUser` with a `StateDirectory`, ships `ffmpeg`
|
||||||
|
on its PATH (timelapses/camera), grants `CAP_NET_BIND_SERVICE` (virtual
|
||||||
|
printer FTPS/RTSP ports), and starts after `network-online.target`.
|
||||||
|
|
||||||
|
### Reverse proxy
|
||||||
|
|
||||||
|
Bambuddy serves everything (UI, API, websockets, camera streams) on one port
|
||||||
|
with absolute `/assets` URLs — proxy `Location /` to `http://127.0.0.1:8000`
|
||||||
|
with websocket upgrade support.
|
||||||
|
|
||||||
|
## Running without NixOS
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#bambuddy # binds 0.0.0.0:8000, state in ~/.local/share/bambuddy
|
||||||
|
PORT=9000 HOST=127.0.0.1 nix run .#bambuddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix flake check # everything
|
||||||
|
nix build .#checks.x86_64-linux.bambuddy-import # backend import smoke test
|
||||||
|
nix build .#checks.x86_64-linux.bambuddy-service # NixOS VM integration test
|
||||||
|
```
|
||||||
|
|
||||||
|
The VM test boots two virtual machines: one with the default configuration
|
||||||
|
(checks `/health`, the React UI, static assets, manifest, SQLite/StateDirectory
|
||||||
|
init and cross-node firewall access) and one exercising the module options
|
||||||
|
(custom host/port, extra environment, loopback-only binding).
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
The package pins an upstream release in `pkgs/bambuddy/default.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
version = "1.2.5.7";
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "maziggy";
|
||||||
|
repo = "bambuddy";
|
||||||
|
rev = "<commit for v${version}>";
|
||||||
|
hash = "sha256-...";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
To bump: change `version` + `rev`, update `hash`, and refresh
|
||||||
|
`npmDepsHash` (a failed build prints the expected value).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Bambuddy is AGPL-3.0; this packaging is provided under the same license.
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1793341285,
|
||||||
|
"narHash": "sha256-Y1CiZ4SoBUjLQyQM/pY+rS2dXD3CHLPstF0lmOc73D4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "1c89083e451e891152365b799595cf7790a43325",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
98
flake.nix
Normal file
98
flake.nix
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
description = "Bambuddy — self-hosted command center for Bambu Lab 3D printers (package + NixOS module)";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs }:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
|
||||||
|
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
bambuddy = pkgs.callPackage ./pkgs/bambuddy { };
|
||||||
|
default = bambuddy;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
bambuddy = self.packages.${final.stdenv.hostPlatform.system}.bambuddy;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules = rec {
|
||||||
|
# Wires the module against this flake's own package as the default,
|
||||||
|
# so `services.bambuddy.enable = true;` works out of the box.
|
||||||
|
bambuddy =
|
||||||
|
{ lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ ./nixos/module.nix ];
|
||||||
|
services.bambuddy.package = lib.mkOptionDefault self.packages.${pkgs.stdenv.hostPlatform.system}.bambuddy;
|
||||||
|
};
|
||||||
|
default = bambuddy;
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
bambuddy = self.packages.${system}.bambuddy;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
bambuddy-import = pkgs.callPackage ./tests/import-check.nix { inherit bambuddy; };
|
||||||
|
}
|
||||||
|
// nixpkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux {
|
||||||
|
bambuddy-service = pkgs.callPackage ./tests/service-test.nix {
|
||||||
|
inherit bambuddy;
|
||||||
|
nixosModule = self.nixosModules.default;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
apps = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
bambuddy = self.packages.${system}.bambuddy;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${bambuddy}/bin/bambuddy";
|
||||||
|
meta.description = "Run the Bambuddy server";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
devShells = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = pkgsFor system;
|
||||||
|
bambuddy = self.packages.${system}.bambuddy;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
bambuddy.passthru.pythonEnv
|
||||||
|
pkgs.nodejs
|
||||||
|
pkgs.ffmpeg
|
||||||
|
pkgs.nixfmt
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
174
nixos/module.nix
Normal file
174
nixos/module.nix
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.services.bambuddy;
|
||||||
|
|
||||||
|
# The default dataDir maps to a systemd StateDirectory, which gets proper
|
||||||
|
# ownership/permission handling for the DynamicUser. Custom paths must be
|
||||||
|
# writable already; we grant access via ReadWritePaths.
|
||||||
|
stateDirName = "bambuddy";
|
||||||
|
defaultDataDir = "/var/lib/${stateDirName}";
|
||||||
|
dataDirManaged = cfg.dataDir == defaultDataDir;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.bambuddy = {
|
||||||
|
enable = lib.mkEnableOption "Bambuddy, a self-hosted command center for Bambu Lab 3D printers";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
description = ''
|
||||||
|
The bambuddy package to run. Defaults to the package built by this
|
||||||
|
flake.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
host = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0.0.0.0";
|
||||||
|
description = ''
|
||||||
|
Address the web UI and API bind to. Use `127.0.0.1` when Bambuddy
|
||||||
|
should only be reachable via a reverse proxy on the same host.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 8000;
|
||||||
|
description = "Port the web UI and API listen on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = defaultDataDir;
|
||||||
|
description = ''
|
||||||
|
Persistent state directory: SQLite database, print archive, plate
|
||||||
|
calibration data and logs live here. The default path under
|
||||||
|
`/var/lib` is created automatically. Custom paths are used as-is and
|
||||||
|
must be writable by the service user (a DynamicUser).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Open the web port in the firewall so the dashboard is reachable from
|
||||||
|
the LAN. Bambuddy itself always initiates printer connections
|
||||||
|
(MQTT/FTP/RTSP), so no other inbound ports are required for printer
|
||||||
|
communication.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
LOG_LEVEL = "DEBUG";
|
||||||
|
VIRTUAL_PRINTER_PASV_ADDRESS = "192.168.1.10";
|
||||||
|
BAMBUDDY_EXTERNAL_ROOTS = "/mnt/nas/3d-prints";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Extra environment variables for Bambuddy. See upstream
|
||||||
|
`.env.example` for the full catalogue (e.g. `DATABASE_URL` for
|
||||||
|
external PostgreSQL, `MFA_ENCRYPTION_KEY`, `SLICER_API_URL`,
|
||||||
|
`BAMBUDDY_EXTERNAL_ROOTS`, `USE_SYSTEM_TRUST_STORE`).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
example = "/var/lib/secrets/bambuddy.env";
|
||||||
|
description = ''
|
||||||
|
Environment file (KEY=value lines) loaded into the service. Useful
|
||||||
|
for secrets that should not live in the world-readable Nix store,
|
||||||
|
such as `MFA_ENCRYPTION_KEY` or OIDC credentials.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraPackages = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.package;
|
||||||
|
default = [ ];
|
||||||
|
example = lib.literalExpression "[ pkgs.tailscale ]";
|
||||||
|
description = ''
|
||||||
|
Additional packages made available on the service PATH (ffmpeg is
|
||||||
|
always included). For example, add `tailscale` when using the
|
||||||
|
virtual printer's Tailscale certificate integration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
|
||||||
|
|
||||||
|
systemd.services.bambuddy = {
|
||||||
|
description = "Bambuddy — self-hosted command center for Bambu Lab 3D printers";
|
||||||
|
documentation = [ "https://wiki.bambuddy.cool" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
HOST = cfg.host;
|
||||||
|
PORT = toString cfg.port;
|
||||||
|
DATA_DIR = cfg.dataDir;
|
||||||
|
LOG_DIR = "${cfg.dataDir}/logs";
|
||||||
|
HOME = cfg.dataDir;
|
||||||
|
MPLCONFIGDIR = "${cfg.dataDir}/.cache/matplotlib";
|
||||||
|
UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN = "10";
|
||||||
|
} // cfg.environment;
|
||||||
|
|
||||||
|
path = [ pkgs.ffmpeg ] ++ cfg.extraPackages;
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = lib.getExe cfg.package;
|
||||||
|
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
|
||||||
|
|
||||||
|
DynamicUser = true;
|
||||||
|
StateDirectory = lib.mkIf dataDirManaged stateDirName;
|
||||||
|
ReadWritePaths = lib.mkIf (!dataDirManaged) [ cfg.dataDir ];
|
||||||
|
|
||||||
|
# The virtual printer FTP (990) and RTSP camera proxy (322) listeners
|
||||||
|
# bind privileged ports as the unprivileged service user — the same
|
||||||
|
# NET_BIND_SERVICE grant the upstream docker-compose.yml uses.
|
||||||
|
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
||||||
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||||
|
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 5;
|
||||||
|
TimeoutStopSec = 30;
|
||||||
|
|
||||||
|
# Hardening. Bambuddy talks to printers over MQTT/FTP/RTSP and shells
|
||||||
|
# out to ffmpeg, hence AF_INET/AF_UNIX and an exec-capable /tmp.
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
RestrictAddressFamilies = [
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
"AF_UNIX"
|
||||||
|
"AF_NETLINK"
|
||||||
|
];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
204
pkgs/bambuddy/default.nix
Normal file
204
pkgs/bambuddy/default.nix
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenv,
|
||||||
|
fetchFromGitHub,
|
||||||
|
buildNpmPackage,
|
||||||
|
python3,
|
||||||
|
makeWrapper,
|
||||||
|
ffmpeg,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
pname = "bambuddy";
|
||||||
|
version = "1.2.5.7";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "maziggy";
|
||||||
|
repo = "bambuddy";
|
||||||
|
rev = "188df76f8a7342542c0876dac68990ea2a8c8313"; # v1.2.5.7
|
||||||
|
hash = "sha256-KmlAdFoSIMa6Wahd4XPBvaBtpVR/5LPNMyaimuDZVb0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Runtime Python dependencies, mirroring upstream requirements.txt.
|
||||||
|
# All of these ship in nixpkgs. Optional heavy extras (opencv4,
|
||||||
|
# fast-simplification) are included for feature parity with the upstream
|
||||||
|
# Docker image, which installs requirements.txt in full; the affected
|
||||||
|
# features (plate detection, STL thumbnails) degrade gracefully if absent.
|
||||||
|
pythonDeps =
|
||||||
|
ps: with ps; [
|
||||||
|
# Web framework
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
uvloop
|
||||||
|
httptools
|
||||||
|
websockets
|
||||||
|
python-dotenv
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sqlalchemy
|
||||||
|
aiosqlite
|
||||||
|
asyncpg
|
||||||
|
greenlet
|
||||||
|
|
||||||
|
# Pydantic / settings
|
||||||
|
pydantic
|
||||||
|
pydantic-settings
|
||||||
|
|
||||||
|
# Bambu Lab printer communication (MQTT / FTP)
|
||||||
|
paho-mqtt
|
||||||
|
aioftp
|
||||||
|
pyftpdlib
|
||||||
|
cryptography
|
||||||
|
pyopenssl
|
||||||
|
asyncssh
|
||||||
|
|
||||||
|
# 3MF processing
|
||||||
|
defusedxml
|
||||||
|
|
||||||
|
# Excel export
|
||||||
|
openpyxl
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
python-multipart
|
||||||
|
aiofiles
|
||||||
|
|
||||||
|
# QR codes / PDF generation / PDF thumbnails
|
||||||
|
qrcode
|
||||||
|
reportlab
|
||||||
|
pypdfium2
|
||||||
|
|
||||||
|
# STL thumbnails
|
||||||
|
trimesh
|
||||||
|
matplotlib
|
||||||
|
fast-simplification
|
||||||
|
networkx
|
||||||
|
lxml
|
||||||
|
|
||||||
|
# System monitoring
|
||||||
|
psutil
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
pyjwt
|
||||||
|
passlib
|
||||||
|
bcrypt
|
||||||
|
ldap3
|
||||||
|
pyotp
|
||||||
|
|
||||||
|
# HTTP clients
|
||||||
|
idna
|
||||||
|
httpx
|
||||||
|
certifi
|
||||||
|
curl-cffi
|
||||||
|
urllib3
|
||||||
|
aiohttp
|
||||||
|
|
||||||
|
# Plate detection
|
||||||
|
opencv4
|
||||||
|
numpy
|
||||||
|
pillow
|
||||||
|
];
|
||||||
|
|
||||||
|
pythonEnv = python3.withPackages pythonDeps;
|
||||||
|
|
||||||
|
|
||||||
|
# Prebuilt React frontend. `vite build` emits to `../static` (the app root,
|
||||||
|
# next to `backend/`), matching the upstream Dockerfile.
|
||||||
|
frontend = buildNpmPackage {
|
||||||
|
pname = "${pname}-web";
|
||||||
|
inherit version src;
|
||||||
|
|
||||||
|
sourceRoot = "${src.name}/frontend";
|
||||||
|
npmDepsHash = "sha256-S+WU4jgs2DPWHHfspeo5k8vPECK+5SidGa8kzI4K4lw=";
|
||||||
|
|
||||||
|
# The source tree arrives from the store read-only, but `vite build`
|
||||||
|
# empties and writes ../static (the repo ships a committed static/ dir).
|
||||||
|
preBuild = ''
|
||||||
|
chmod -R u+w "$NIX_BUILD_TOP/${src.name}"
|
||||||
|
rm -rf ../static
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out/share
|
||||||
|
cp -r ../static $out/share/static
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Web frontend for Bambuddy";
|
||||||
|
license = lib.licenses.agpl3Only;
|
||||||
|
platforms = lib.platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
inherit pname version src;
|
||||||
|
|
||||||
|
dontBuild = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = [ makeWrapper ];
|
||||||
|
|
||||||
|
buildInputs = [ pythonEnv ];
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
inherit pythonEnv frontend;
|
||||||
|
};
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
# App root: `backend/` (Python sources) + `static/` (prebuilt frontend).
|
||||||
|
# backend.app.core.config resolves both relative to this root, exactly
|
||||||
|
# like the upstream Docker image (/app/backend + /app/static).
|
||||||
|
mkdir -p $out/share/bambuddy
|
||||||
|
cp -r ${src}/backend $out/share/bambuddy/backend
|
||||||
|
cp -r ${frontend}/share/static $out/share/bambuddy/static
|
||||||
|
chmod -R u+w $out/share/bambuddy
|
||||||
|
|
||||||
|
# The upstream test suite is not shipped in the runtime package.
|
||||||
|
rm -rf $out/share/bambuddy/backend/tests
|
||||||
|
find $out/share/bambuddy -type d -name __pycache__ -prune -exec rm -rf {} +
|
||||||
|
|
||||||
|
install -Dm644 ${src}/LICENSE -t $out/share/licenses/bambuddy
|
||||||
|
|
||||||
|
mkdir -p $out/bin
|
||||||
|
install -Dm755 ${./wrapper.sh} $out/bin/bambuddy
|
||||||
|
# Substitute at build time so @out@ resolves to THIS package's path.
|
||||||
|
# (A separate replaceVars derivation would substitute its own outPath
|
||||||
|
# instead — which silently broke the PYTHONPATH in the launcher.)
|
||||||
|
substituteInPlace $out/bin/bambuddy \
|
||||||
|
--replace-warn '@out@' "$out" \
|
||||||
|
--replace-warn '@pythonEnv@' "${pythonEnv}"
|
||||||
|
# ffmpeg is used for timelapse conversion and camera stream re-encoding.
|
||||||
|
wrapProgram $out/bin/bambuddy \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ ffmpeg ]}
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Fix up the interpreter reference in the substituted wrapper.
|
||||||
|
preFixup = ''
|
||||||
|
patchShebangs $out/bin/bambuddy
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Self-hosted command center for Bambu Lab 3D printers";
|
||||||
|
longDescription = ''
|
||||||
|
Bambuddy is a self-hosted command center for Bambu Lab 3D printers —
|
||||||
|
from a single A1 to an entire print farm. Monitor printers in real
|
||||||
|
time, archive every print automatically, slice and queue jobs locally,
|
||||||
|
and manage the whole workflow without Bambu's cloud.
|
||||||
|
|
||||||
|
This package bundles the FastAPI backend and the prebuilt React
|
||||||
|
frontend behind a single `bambuddy` launcher. Configuration is done via
|
||||||
|
environment variables (DATA_DIR, LOG_DIR, HOST, PORT, ...) — see the
|
||||||
|
NixOS module (`services.bambuddy`) or upstream `.env.example` for the
|
||||||
|
full list.
|
||||||
|
'';
|
||||||
|
homepage = "https://bambuddy.cool/";
|
||||||
|
changelog = "https://github.com/maziggy/bambuddy/releases/tag/v${version}";
|
||||||
|
license = lib.licenses.agpl3Only;
|
||||||
|
sourceProvenance = with lib.sourceTypes; [ fromSource ];
|
||||||
|
platforms = lib.platforms.unix;
|
||||||
|
mainProgram = "bambuddy";
|
||||||
|
};
|
||||||
|
}
|
||||||
28
pkgs/bambuddy/wrapper.sh
Normal file
28
pkgs/bambuddy/wrapper.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bambuddy launcher.
|
||||||
|
#
|
||||||
|
# Starts the FastAPI backend via uvicorn, mirroring the upstream Docker CMD
|
||||||
|
# (`uvicorn backend.app.main:app --loop asyncio`). PYTHONPATH points at the
|
||||||
|
# app root (@out@/share/bambuddy) which contains the `backend` package and
|
||||||
|
# the prebuilt `static/` frontend — the layout backend.app.core.config expects.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# State locations. The NixOS module always sets these explicitly; the
|
||||||
|
# defaults keep a bare `bambuddy` invocation out of the read-only store.
|
||||||
|
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bambuddy}"
|
||||||
|
export LOG_DIR="${LOG_DIR:-$DATA_DIR/logs}"
|
||||||
|
export HOME="${HOME:-$DATA_DIR}"
|
||||||
|
export MPLCONFIGDIR="${MPLCONFIGDIR:-$DATA_DIR/.cache/matplotlib}"
|
||||||
|
|
||||||
|
mkdir -p "$DATA_DIR" "$LOG_DIR" "$MPLCONFIGDIR"
|
||||||
|
|
||||||
|
export PYTHONPATH="@out@/share/bambuddy${PYTHONPATH:+:$PYTHONPATH}"
|
||||||
|
|
||||||
|
# `--loop asyncio` matches the upstream Dockerfile: uvloop has a TLS bug that
|
||||||
|
# can truncate VP FTP uploads. Override with UVICORN_LOOP=uvloop if desired.
|
||||||
|
exec "@pythonEnv@/bin/uvicorn" backend.app.main:app \
|
||||||
|
--host "${HOST:-0.0.0.0}" \
|
||||||
|
--port "${PORT:-8000}" \
|
||||||
|
--loop "${UVICORN_LOOP:-asyncio}" \
|
||||||
|
--timeout-graceful-shutdown "${UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN:-5}" \
|
||||||
|
"$@"
|
||||||
27
tests/import-check.nix
Normal file
27
tests/import-check.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
runCommand,
|
||||||
|
bambuddy,
|
||||||
|
}:
|
||||||
|
# Fast package-level smoke test: import the full backend (which pulls in
|
||||||
|
# every router, service and optional dependency) inside a sandbox with a
|
||||||
|
# writable state dir, and exercise the admin CLI entrypoint. Catches broken
|
||||||
|
# Python dependency closures long before the (much slower) VM test runs.
|
||||||
|
runCommand "bambuddy-import-check"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [ bambuddy.passthru.pythonEnv ];
|
||||||
|
meta = {
|
||||||
|
description = "Bambuddy: verify the backend imports with the packaged dependency closure";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export DATA_DIR="$PWD/state" LOG_DIR="$PWD/state/logs" HOME="$PWD/state" MPLCONFIGDIR="$PWD/state/mpl"
|
||||||
|
mkdir -p "$DATA_DIR" "$LOG_DIR"
|
||||||
|
export PYTHONPATH="${bambuddy}/share/bambuddy"
|
||||||
|
|
||||||
|
python -c 'import backend.app.main; print("backend.app.main imports cleanly")'
|
||||||
|
|
||||||
|
python -m backend.app.cli --help > /dev/null
|
||||||
|
|
||||||
|
touch $out
|
||||||
|
''
|
||||||
92
tests/service-test.nix
Normal file
92
tests/service-test.nix
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
bambuddy,
|
||||||
|
nixosModule,
|
||||||
|
}:
|
||||||
|
# Integration test: boots two NixOS VMs with the bambuddy NixOS module.
|
||||||
|
#
|
||||||
|
# * `server` uses the default configuration (port 8000, firewall open) and
|
||||||
|
# checks that the service starts, the API responds, the prebuilt React
|
||||||
|
# frontend is served with its static assets, and persistent state
|
||||||
|
# (SQLite database, logs) lands in the StateDirectory.
|
||||||
|
# * `custom` exercises the module options (custom host/port, extra
|
||||||
|
# environment) and verifies the dashboard is reachable cross-node once
|
||||||
|
# the firewall is opened.
|
||||||
|
pkgs.testers.runNixOSTest (
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
name = "bambuddy-service";
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
server =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ nixosModule ];
|
||||||
|
services.bambuddy = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
environment.systemPackages = [ pkgs.curl ];
|
||||||
|
};
|
||||||
|
|
||||||
|
custom =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ nixosModule ];
|
||||||
|
services.bambuddy = {
|
||||||
|
enable = true;
|
||||||
|
host = "127.0.0.1";
|
||||||
|
port = 8123;
|
||||||
|
environment.LOG_LEVEL = "DEBUG";
|
||||||
|
};
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
curl
|
||||||
|
iproute2
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
server.start()
|
||||||
|
custom.start()
|
||||||
|
|
||||||
|
server.wait_for_unit("bambuddy.service")
|
||||||
|
server.wait_for_open_port(8000)
|
||||||
|
|
||||||
|
with subtest("health endpoint responds"):
|
||||||
|
server.succeed("curl -fsS http://127.0.0.1:8000/health | grep -q healthy")
|
||||||
|
|
||||||
|
with subtest("uvicorn started cleanly"):
|
||||||
|
server.succeed(
|
||||||
|
"journalctl -u bambuddy.service --no-pager | grep -q 'Application startup complete'"
|
||||||
|
)
|
||||||
|
|
||||||
|
with subtest("web UI is served from the packaged static build"):
|
||||||
|
server.succeed("curl -fsS http://127.0.0.1:8000/ | grep -q '<title>Bambuddy</title>'")
|
||||||
|
server.succeed("curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/img/favicon-32x32.png | grep -q '^200$'")
|
||||||
|
server.succeed("curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/fonts/inter-latin.woff2 | grep -q '^200$'")
|
||||||
|
server.succeed("curl -fsS http://127.0.0.1:8000/manifest.json | grep -qi bambuddy")
|
||||||
|
|
||||||
|
with subtest("persistent state lands in the StateDirectory"):
|
||||||
|
server.succeed("test -f /var/lib/bambuddy/bambuddy.db")
|
||||||
|
server.succeed("test -d /var/lib/bambuddy/logs")
|
||||||
|
|
||||||
|
with subtest("dashboard is reachable from another machine (openFirewall)"):
|
||||||
|
custom.succeed("curl -m 10 -fsS http://server:8000/health | grep -q healthy")
|
||||||
|
|
||||||
|
with subtest("custom host/port options apply"):
|
||||||
|
custom.wait_for_unit("bambuddy.service")
|
||||||
|
custom.wait_for_open_port(8123)
|
||||||
|
custom.succeed("curl -fsS http://127.0.0.1:8123/health | grep -q healthy")
|
||||||
|
# host = 127.0.0.1 must not appear on any external address
|
||||||
|
custom.fail("ss -tln | grep -E '0\\.0\\.0\\.0:8123|\\[::\\]:8123|\\*:8123'")
|
||||||
|
server.fail("curl -m 5 -fsS http://custom:8123/health")
|
||||||
|
|
||||||
|
with subtest("extra environment reaches the service"):
|
||||||
|
custom.succeed(
|
||||||
|
"systemctl show bambuddy.service -p Environment | grep -q LOG_LEVEL=DEBUG"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue