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 <$tmp/sites/common_site_config.json < $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="; +}