From f8346df08f361c9e507920818332eb24b85bd8b1 Mon Sep 17 00:00:00 2001
From: teutat3s <teutates@mailbox.org>
Date: Tue, 23 May 2023 00:50:17 +0200
Subject: [PATCH] wip: backend works. frontend still has some issues

---
 flake.nix              |  35 +++++++--
 nginx-erpnext-conf.nix | 157 +++++++++++++++++++++++++++++++++++++++++
 node/frappe-assets.nix |   6 ++
 python.nix             |   2 +
 python/bench.nix       |  39 ++++++++++
 python/erpnext.nix     |   8 ---
 python/honcho.nix      |  17 +++++
 srcs/bench.nix         |   7 ++
 8 files changed, 256 insertions(+), 15 deletions(-)
 create mode 100644 nginx-erpnext-conf.nix
 create mode 100644 python/bench.nix
 create mode 100644 python/honcho.nix
 create mode 100644 srcs/bench.nix

diff --git a/flake.nix b/flake.nix
index 76bb443..2426839 100644
--- a/flake.nix
+++ b/flake.nix
@@ -26,17 +26,26 @@
         #  propagatedBuildInputs = old.propagatedBuildInputs ++ [python3.pkgs.setuptools];
         #});
         assets = pkgs.callPackage ./node/frappe-assets.nix {};
+        # Source: https://github.com/frappe/frappe_docker/blob/main/resources/nginx-template.conf
+        nginx-conf = pkgs.callPackage ./nginx-erpnext-conf.nix {inherit pkgs;};
         penv = py.buildEnv.override {
           extraLibs = [ py.pkgs.frappe py.pkgs.erpnext ];
         };
         runErpNext = pkgs.writeShellScriptBin "runErpNext" ''
           export PYTHON_PATH=${penv}/${py.sitePackages}
+          # The upstream installer bench CLI wants mysql in its PATH
+          export PATH=${pkgs.mariadb-client}/bin:''$PATH
+
           hostname=localhost
-          sites=$(mktemp -d)
+          tmp=/tmp/erpnext
+
+          mkdir -p $tmp/apps $tmp/sites $tmp/config/pids $tmp/logs $tmp/env/bin
+
           for f in ${assets}/share/sites/*; do
-              ln -s "$f" "$sites/$(basename $f)"
+              ln -s "$f" "$tmp/sites/$(basename $f)"
           done
-          cat >$sites/common_site_config.json <<EOF
+
+          cat >$tmp/sites/common_site_config.json <<EOF
           {
             "db_host": "localhost",
             "db_port": 3306,
@@ -48,10 +57,21 @@
             "socketio_port": 6379
           }
           EOF
-          mkdir -p $sites/$hostname/logs
-          ln -s $sites/common_site_config.json $sites/$hostname/site_config.json
-          echo "Sites dir: $sites"
-          ${penv}/bin/gunicorn --chdir="$sites" --bind=0.0.0.0:9090 --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application
+
+          # The upstream bench CLI installer expects this file
+          echo -e "erpnext\nfrappe\n" > $tmp/sites/apps.txt
+
+          cd $tmp
+          ln -s ${py.pkgs.erpnext}/lib/python3.10/site-packages apps/erpnext
+          ln -s ${py.pkgs.frappe}/lib/python3.10/site-packages apps/frappe
+          ln -s ${penv} $tmp/env
+          ln -sf ${nginx-conf} $tmp/nginx-erpnext.conf
+
+          # Upstream initializes the DB with this command
+          ${py.pkgs.bench}/bin/bench new-site localhost --mariadb-root-password password --admin-password admin
+
+          echo "Workdir: $tmp"
+          ${penv}/bin/gunicorn --chdir="$tmp/sites" --bind=0.0.0.0:9090 --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application
         '';
       in rec {
         packages = {
@@ -64,6 +84,7 @@
           inherit pkgs runErpNext assets;
           pip2nix = import "${pip2nix}/default.nix" { inherit pkgs; pythonPackages = "python310Packages"; };
           erpnext = py.pkgs.erpnext;
+          bench = py.pkgs.bench;
           pythonPkgs = py.pkgs;
         };
     });
diff --git a/nginx-erpnext-conf.nix b/nginx-erpnext-conf.nix
new file mode 100644
index 0000000..76c6eeb
--- /dev/null
+++ b/nginx-erpnext-conf.nix
@@ -0,0 +1,157 @@
+{ pkgs }:
+let
+  backend = "localhost:9090";
+  socketio = "0.0.0.0:9000";
+  frappe_site_name_header = "localhost";
+  upstream_real_ip_address = "127.0.0.1";
+  upstream_real_ip_header = "X-Forwarded-For";
+  upstream_real_ip_recursive = "off";
+  client_max_body_size = "50m";
+  proxy_read_timeout = "120";
+in
+pkgs.writeText "erpnext.conf" ''
+user  nginx;
+worker_processes  auto;
+
+error_log  /tmp/erpnext/logs/nginx/error.log notice;
+pid        /tmp/erpnext/nginx.pid;
+
+
+events {
+    worker_connections  1024;
+}
+
+
+http {
+    include       ${pkgs.nginx}/conf/mime.types;
+    default_type  application/octet-stream;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /tmp/erpnext/logs/nginx/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout  65;
+
+    #gzip  on;
+
+    upstream backend-server {
+            server ${backend} fail_timeout=0;
+    }
+
+    upstream socketio-server {
+            server ${socketio} fail_timeout=0;
+    }
+
+    # Parse the X-Forwarded-Proto header - if set - defaulting to $scheme.
+    map $http_x_forwarded_proto $proxy_x_forwarded_proto {
+            default $scheme;
+            https https;
+    }
+
+    server {
+            listen 8080;
+            server_name ${frappe_site_name_header};
+            root /tmp/erpnext/sites;
+
+            proxy_buffer_size 128k;
+            proxy_buffers 4 256k;
+            proxy_busy_buffers_size 256k;
+
+            add_header X-Frame-Options "SAMEORIGIN";
+            add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
+            add_header X-Content-Type-Options nosniff;
+            add_header X-XSS-Protection "1; mode=block";
+            add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin";
+
+            set_real_ip_from ${upstream_real_ip_address};
+            real_ip_header ${upstream_real_ip_header};
+            real_ip_recursive ${upstream_real_ip_recursive};
+
+            location /assets {
+                    try_files $uri =404;
+            }
+
+            location ~ ^/protected/(.*) {
+                    internal;
+                    try_files /${frappe_site_name_header}/$1 =404;
+            }
+
+            location /socket.io {
+                    proxy_http_version 1.1;
+                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+                    proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
+                    proxy_set_header Upgrade $http_upgrade;
+                    proxy_set_header Connection "upgrade";
+                    proxy_set_header X-Frappe-Site-Name ${frappe_site_name_header};
+                    proxy_set_header Origin $scheme://${frappe_site_name_header};
+                    proxy_set_header Host $host;
+
+                    proxy_pass http://socketio-server;
+            }
+
+            location / {
+                    rewrite ^(.+)/$ $proxy_x_forwarded_proto://${frappe_site_name_header}$1 permanent;
+                    rewrite ^(.+)/index\.html$ $proxy_x_forwarded_proto://${frappe_site_name_header}$1 permanent;
+                    rewrite ^(.+)\.html$ $proxy_x_forwarded_proto://${frappe_site_name_header}$1 permanent;
+
+                    location ~ ^/files/.*.(htm|html|svg|xml) {
+                            add_header Content-disposition "attachment";
+                            try_files /${frappe_site_name_header}/public/$uri @webserver;
+                    }
+
+                    try_files /${frappe_site_name_header}/public/$uri @webserver;
+            }
+
+            location @webserver {
+                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+                    proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
+                    proxy_set_header X-Frappe-Site-Name ${frappe_site_name_header};
+                    proxy_set_header Host $host;
+                    proxy_set_header X-Use-X-Accel-Redirect True;
+                    proxy_read_timeout ${proxy_read_timeout};
+                    proxy_redirect off;
+
+                    proxy_pass  http://backend-server;
+            }
+
+            # optimizations
+            sendfile on;
+            keepalive_timeout 15;
+            client_max_body_size ${client_max_body_size};
+            client_body_buffer_size 16K;
+            client_header_buffer_size 1k;
+
+            # enable gzip compression
+            # based on https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge
+            gzip on;
+            gzip_http_version 1.1;
+            gzip_comp_level 5;
+            gzip_min_length 256;
+            gzip_proxied any;
+            gzip_vary on;
+            gzip_types
+                    application/atom+xml
+                    application/javascript
+                    application/json
+                    application/rss+xml
+                    application/vnd.ms-fontobject
+                    application/x-font-ttf
+                    application/font-woff
+                    application/x-web-app-manifest+json
+                    application/xhtml+xml
+                    application/xml
+                    font/opentype
+                    image/svg+xml
+                    image/x-icon
+                    text/css
+                    text/plain
+                    text/x-component;
+                    # text/html is always compressed by HttpGzipModule
+    }
+}
+''
diff --git a/node/frappe-assets.nix b/node/frappe-assets.nix
index 5ad410a..ff72a8f 100644
--- a/node/frappe-assets.nix
+++ b/node/frappe-assets.nix
@@ -64,6 +64,8 @@ let
         cp -r "${erpnextSrc}/erpnext/public" "sites/assets/erpnext"
         cp -r "$src/frappe/public" "sites/assets/frappe"
 
+        # The upstream esbuild script expects this file and will build assets
+        # for all listed apps
         echo -e "erpnext\nfrappe\n" > sites/apps.txt
 
         cp -r ${erpnextSrc} apps/erpnext
@@ -78,6 +80,10 @@ let
         cp -r $node_modules apps/frappe/node_modules
 
         yarn --offline production
+
+        # Clean up
+        rm sites/apps.txt
+
         mv sites deps/
 
         runHook postBuild
diff --git a/python.nix b/python.nix
index 4f56d99..d6b6fc7 100644
--- a/python.nix
+++ b/python.nix
@@ -2,12 +2,14 @@
 
 pkgs.python3.override {
   packageOverrides = self: super: {
+    bench = self.callPackage ./python/bench.nix {};
     erpnext = self.callPackage ./python/erpnext.nix {};
     frappe = self.callPackage ./python/frappe.nix {};
 
     email-reply-parser = self.callPackage ./python/email-reply-parser.nix {};
     git-url-parse = self.callPackage ./python/git-url-parse.nix {};
     gocardless-pro = self.callPackage ./python/gocardless-pro.nix {};
+    honcho = self.callPackage ./python/honcho.nix {};
     jsonobject = self.callPackage ./python/jsonobject.nix {};
     maxminddb-geolite2 = self.callPackage ./python/maxminddb-geolite2.nix {};
     posthog = self.callPackage ./python/posthog.nix {};
diff --git a/python/bench.nix b/python/bench.nix
new file mode 100644
index 0000000..d3a3217
--- /dev/null
+++ b/python/bench.nix
@@ -0,0 +1,39 @@
+{ lib
+, buildPythonPackage
+, fetchFromGitHub
+, pythonRelaxDepsHook
+
+, hatchling
+
+, click
+, gitpython
+, honcho
+, jinja2
+, python-crontab
+, requests
+, semantic-version
+, setuptools
+, tomli
+}:
+buildPythonPackage rec {
+  pname = "frappe-bench";
+  version = "5.16.2";
+  format = "pyproject";
+  src = import ../srcs/bench.nix {inherit fetchFromGitHub; };
+  nativeBuildInputs = [ pythonRelaxDepsHook ];
+  pythonRelaxDeps = [ "jinja2" "python-crontab" "semantic-version" ];
+  buildInputs = [
+    hatchling
+  ];
+  propagatedBuildInputs = [
+    click
+    gitpython
+    honcho
+    jinja2
+    python-crontab
+    requests
+    semantic-version
+    setuptools
+    tomli
+  ];
+}
diff --git a/python/erpnext.nix b/python/erpnext.nix
index 7bd7527..1a06ca1 100644
--- a/python/erpnext.nix
+++ b/python/erpnext.nix
@@ -1,8 +1,6 @@
 { lib
 , buildPythonPackage
 , fetchFromGitHub
-, fetchYarnDeps
-, mkYarnPackage
 
 , taxjar
 , gocardless-pro
@@ -34,10 +32,4 @@ buildPythonPackage rec {
     python-stdnum
     frappe
   ];
-
-  # postInstall = ''
-  #   mkdir -p $out/test/frappe $out/test/erpnext
-  #   ln -s ${frappe-assets} $out/test/frappe
-  #   ln -s ${erpnext-modules} $out/test/erpnext
-  # '';
 }
diff --git a/python/honcho.nix b/python/honcho.nix
new file mode 100644
index 0000000..a0e9c74
--- /dev/null
+++ b/python/honcho.nix
@@ -0,0 +1,17 @@
+{
+  buildPythonPackage,
+  fetchPypi,
+  jinja2,
+}:
+buildPythonPackage rec {
+  pname = "honcho";
+  version = "1.1.0";
+  src = fetchPypi {
+    pname = "honcho";
+    inherit version;
+    sha256 = "sha256-xeygve1L72aXojrsBCL9T2UI6jWBl5o0hfxLiTV+sqk=";
+  };
+  propagatedBuildInputs = [
+    jinja2
+  ];
+}
diff --git a/srcs/bench.nix b/srcs/bench.nix
new file mode 100644
index 0000000..da5ebc9
--- /dev/null
+++ b/srcs/bench.nix
@@ -0,0 +1,7 @@
+{fetchFromGitHub}:
+fetchFromGitHub {
+  owner = "frappe";
+  repo = "bench";
+  rev = "v5.16.2";
+  sha256 = "sha256-SF/RwY54OKXTDIYz4LsQIR03QoCKPIGey60DO8TdonY=";
+}