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