aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoufic ar <contact@toufy.me>2026-05-13 23:16:11 +0300
committertoufic ar <contact@toufy.me>2026-05-13 23:16:11 +0300
commitb8d6c62f731c3c7886f2ecc661265eedeb65e41c (patch)
tree88e9b424e51197af993572a722f1e5c1d1bda758
parentaca0b3964d79e58aa1b319cbdbd7b1513ba9cfd8 (diff)
downloadmakeshiftci-b8d6c62f731c3c7886f2ecc661265eedeb65e41c.tar.gz
makeshiftci-b8d6c62f731c3c7886f2ecc661265eedeb65e41c.zip
test flake webui
-rw-r--r--flake.nix110
-rw-r--r--pyproject.toml4
-rw-r--r--uv.lock29
-rw-r--r--web.py45
4 files changed, 147 insertions, 41 deletions
diff --git a/flake.nix b/flake.nix
index f4789d2..bdea298 100644
--- a/flake.nix
+++ b/flake.nix
@@ -3,18 +3,28 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ pyproject-nix = {
+ url = "github:nix-community/pyproject.nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
};
outputs = {
self,
nixpkgs,
+ pyproject-nix,
}: let
system = "x86_64-linux";
pkgs = import nixpkgs {inherit system;};
+ python = pkgs.python3;
msci = (pkgs.writeScriptBin
"msci" (builtins.readFile ./msci)).overrideAttrs (old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
+ msci-web = pyproject-nix.lib.project.loadPyproject {
+ projectRoot = ./.;
+ };
+ msci-web-attrs = msci-web.renderers.buildPythonPackage {inherit python;};
in {
packages."${system}".msci = pkgs.symlinkJoin {
name = "msci";
@@ -22,6 +32,8 @@
buildInputs = [pkgs.makeWrapper];
postBuild = "wrapProgram $out/bin/msci --prefix PATH : $out/bin";
};
+ packages."${system}".msci-web =
+ python.pkgs.buildPythonPackage msci-web-attrs;
nixosModules.default = {
lib,
config,
@@ -40,40 +52,86 @@
description = "data directory of makeshiftci";
};
createUser = mkEnableOption "create a non-root user";
+ webUI = mkOption {
+ type = types.submodule {
+ options = {
+ enable = mkEnableOption "enable makeshiftci web UI";
+ port = mkOption {
+ type = types.int;
+ default = 5000;
+ description = "port to run the web UI on";
+ };
+ timeout = mkOption {
+ type = types.int;
+ default = 3600;
+ description = "gunicorn timeout";
+ };
+ workers = mkOption {
+ type = types.int;
+ default = 4;
+ description = "number of gunicorn workers";
+ };
+ };
+ };
+ default = {};
+ };
};
};
default = {};
};
};
- config = lib.mkIf cfg.enable {
- environment = {
- variables.MSCI_HOME = cfg.dataDir;
- systemPackages = [self.packages."${system}".msci];
- };
- systemd.tmpfiles.settings."makeshiftci" = {
- "${cfg.dataDir}" = {
- d = {
- user =
- if cfg.createUser
- then "makeshiftci"
- else "root";
- group =
- if cfg.createUser
- then "makeshiftci"
- else "root";
- mode = "0750";
+ config = lib.mkIf cfg.enable ({
+ environment = {
+ variables.MSCI_HOME = cfg.dataDir;
+ systemPackages = [self.packages."${system}".msci];
+ };
+ systemd.tmpfiles.settings."makeshiftci" = {
+ "${cfg.dataDir}" = {
+ d = {
+ user =
+ if cfg.createUser
+ then "makeshiftci"
+ else "root";
+ group =
+ if cfg.createUser
+ then "makeshiftci"
+ else "root";
+ mode = "0750";
+ };
};
};
- };
- services.cron.enable = true;
- users = lib.mkIf cfg.createUser {
- users."makeshiftci" = {
- group = "makeshiftci";
- home = cfg.dataDir;
- useDefaultShell = true;
+ services.cron.enable = true;
+ users = lib.mkIf cfg.createUser {
+ users."makeshiftci" = {
+ group = "makeshiftci";
+ home = cfg.dataDir;
+ useDefaultShell = true;
+ };
};
- };
- };
+ }
+ // lib.mkIf cfg.webUI.enable {
+ systemd.services.makeshiftci-web = {
+ wantedBy = ["multi-user.target"];
+ unitConfig.ConditionUser =
+ if cfg.createUser
+ then "makeshiftci"
+ else "root";
+ serviceConfig = let
+ webui = pkgs.python3.withPackages (p:
+ with p; [
+ gunicorn
+ (callPackage self.packages."${system}".msci-web {})
+ ]);
+ in {
+ ExecStart =
+ "${webui}/bin/gunicorn "
+ + "-w ${cfg.webUI.workers} "
+ + "--timeout ${cfg.webUI.timeout} "
+ + "-b 127.0.0.1:${cfg.webUI.port} "
+ + "'web:create_app()'";
+ };
+ };
+ });
};
};
}
diff --git a/pyproject.toml b/pyproject.toml
index 68655cf..08f07c1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,3 +7,7 @@ requires-python = ">=3.12"
dependencies = [
"flask>=3.1.3",
]
+[dependency-groups]
+dev = [
+ "gunicorn>=26.0.0",
+]
diff --git a/uv.lock b/uv.lock
index 9b7d700..01b7532 100644
--- a/uv.lock
+++ b/uv.lock
@@ -50,6 +50,18 @@ wheels = [
]
[[package]]
+name = "gunicorn"
+version = "26.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
+]
+
+[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
@@ -78,9 +90,17 @@ dependencies = [
{ name = "flask" },
]
+[package.dev-dependencies]
+dev = [
+ { name = "gunicorn" },
+]
+
[package.metadata]
requires-dist = [{ name = "flask", specifier = ">=3.1.3" }]
+[package.metadata.requires-dev]
+dev = [{ name = "gunicorn", specifier = ">=26.0.0" }]
+
[[package]]
name = "markupsafe"
version = "3.0.3"
@@ -145,6 +165,15 @@ wheels = [
]
[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
name = "werkzeug"
version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
diff --git a/web.py b/web.py
index 0bbebb7..3daef78 100644
--- a/web.py
+++ b/web.py
@@ -13,20 +13,22 @@ from flask import (
stream_template,
)
+MSCI_HOME = os.environ.get("MSCI_HOME")
+
def get_project(project: str) -> dict[str, Any]:
- with open(f"./projects/{project}.json", "r") as p:
+ with open(f"{MSCI_HOME}/projects/{project}.json", "r") as p:
return json.loads(p.read())
def get_run(project: str, run: int) -> str:
- with open(f"./stdout/public/{project}/{run}", "r") as r:
+ with open(f"{MSCI_HOME}/stdout/public/{project}/{run}", "r") as r:
return r.read()
def stream_run(project: str, run: int):
while True:
- with open(f"./stdout/public/{project}/{run}", "r") as r:
+ with open(f"{MSCI_HOME}/stdout/public/{project}/{run}", "r") as r:
txt = r.read()
yield txt
if "--MSCI_EXIT_" in txt:
@@ -35,14 +37,14 @@ def stream_run(project: str, run: int):
def _projects(q: queue.Queue[tuple[str, dict[str, Any]] | None]):
- if os.path.isdir("./projects"):
- _projects = os.listdir("./projects")
+ if os.path.isdir(f"{MSCI_HOME}/projects"):
+ _projects = os.listdir(f"{MSCI_HOME}/projects")
for _p in _projects:
loaded = get_project(_p.replace(".json", ""))
if not loaded["hidden"]:
q.put((_p.replace(".json", ""), loaded))
else:
- os.mkdir("./projects")
+ os.mkdir(f"{MSCI_HOME}/projects")
q.put(None)
@@ -57,7 +59,7 @@ def projects():
def _project_runs(project: str, q: queue.Queue[dict[str, Any] | None]):
- pr_stdout = f"./stdout/public/{project}"
+ pr_stdout = f"{MSCI_HOME}/stdout/public/{project}"
if os.path.isdir(pr_stdout):
_runs = sorted(os.listdir(pr_stdout), key=int)
for _r in _runs:
@@ -96,37 +98,43 @@ def project_runs(project: str):
def project_exists(name: str):
return (
- os.path.isfile(f"./projects/{name}.json")
+ os.path.isfile(f"{MSCI_HOME}/projects/{name}.json")
and get_project(name)["hidden"] == False
)
def run_exists(project: str, run: int):
if project_exists(project):
- if not os.path.isfile(f"./stdout/public/{project}/{run}"):
+ if not os.path.isfile(f"{MSCI_HOME}/stdout/public/{project}/{run}"):
return False
return True
return False
-app = Flask(__name__)
+_app = Flask(__name__)
+_empty = Flask(__name__)
+
+
+@_empty.route("/", methods=["GET"])
+def ey():
+ return "MSCI_HOME not set"
-@app.route("/favicon.ico")
+@_app.route("/favicon.ico")
def favicon():
return send_from_directory(
- app.root_path,
+ _app.root_path,
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
-@app.route("/", methods=["GET"])
+@_app.route("/", methods=["GET"])
def home():
return stream_template("home.html", projects=projects())
-@app.route("/<string:project>", methods=["GET"])
+@_app.route("/<string:project>", methods=["GET"])
def project(project: str):
if project_exists(project):
return stream_template(
@@ -138,7 +146,7 @@ def project(project: str):
abort(404)
-@app.route("/<string:project>/<int:run>", methods=["GET"])
+@_app.route("/<string:project>/<int:run>", methods=["GET"])
def run(project: str, run: int):
if run_exists(project, run):
return stream_template(
@@ -149,3 +157,10 @@ def run(project: str, run: int):
run_number=run,
)
abort(404)
+
+
+def create_app():
+ if not MSCI_HOME:
+ return _empty
+ else:
+ return _app