From a3b8665f77378923508240c091db5a07ee6118b7 Mon Sep 17 00:00:00 2001 From: sinavir Date: Mon, 29 Dec 2025 07:03:37 +0100 Subject: [PATCH 1/6] maintainers: add sinavir --- maintainers/maintainer-list.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 8702379d3f11..7ab8ba9565a4 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -26923,6 +26923,12 @@ github = "sinanmohd"; githubId = 69694713; }; + sinavir = { + name = "Sinavir"; + email = "sinavir@sinavir.fr"; + github = "sinavir"; + githubId = 36380103; + }; sinics = { name = "Zhifan"; email = "nonno.felice69uwu@gmail.com"; From 94783c7eb2653145b4d1050d0ed5fd78b23bcdec Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 23 Sep 2026 01:45:12 +0200 Subject: [PATCH 2/6] python3Packages.friendly-traceback: init at 0-unstable-2025-04-13 --- .../friendly-traceback/default.nix | 48 +++++++++++++++++++ pkgs/top-level/python-packages.nix | 2 + 2 files changed, 50 insertions(+) create mode 100644 pkgs/development/python-modules/friendly-traceback/default.nix diff --git a/pkgs/development/python-modules/friendly-traceback/default.nix b/pkgs/development/python-modules/friendly-traceback/default.nix new file mode 100644 index 000000000000..652d8b32c481 --- /dev/null +++ b/pkgs/development/python-modules/friendly-traceback/default.nix @@ -0,0 +1,48 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + wheel, + asttokens, + executing, + pure-eval, + stack-data, +}: + +buildPythonPackage { + pname = "friendly-traceback"; + version = "0-unstable-2025-04-13"; + pyproject = true; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "friendly-traceback"; + repo = "friendly-traceback"; + rev = "bba8fc43c4caa4b64e8800e0b1f7ed39e0276170"; + hash = "sha256-ThYE4JVPzTmot9mYB2LN2NmU/Wqd71Spv9+VAP9/Zp0="; + }; + + build-system = [ + setuptools + wheel + ]; + + dependencies = [ + asttokens + executing + pure-eval + stack-data + ]; + + pythonImportsCheck = [ + "friendly_traceback" + ]; + + meta = { + description = "Friendlier Python tracebacks"; + homepage = "https://github.com/friendly-traceback/friendly-traceback"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ sinavir ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 33447fd4ffaa..bcc29b294f5a 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -6524,6 +6524,8 @@ self: super: with self; { frida-python = callPackage ../development/python-modules/frida-python { }; + friendly-traceback = callPackage ../development/python-modules/friendly-traceback { }; + frigidaire = callPackage ../development/python-modules/frigidaire { }; frilouz = callPackage ../development/python-modules/frilouz { }; From 2e46e9899db8203734826016d584b2f8f3e8410e Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 23 Sep 2026 01:46:09 +0200 Subject: [PATCH 3/6] python3Packages.pygrister: init at 0.10.0 --- .../python-modules/pygrister/default.nix | 55 +++++++++++++++++++ pkgs/top-level/python-packages.nix | 2 + 2 files changed, 57 insertions(+) create mode 100644 pkgs/development/python-modules/pygrister/default.nix diff --git a/pkgs/development/python-modules/pygrister/default.nix b/pkgs/development/python-modules/pygrister/default.nix new file mode 100644 index 000000000000..4b3a220c1e77 --- /dev/null +++ b/pkgs/development/python-modules/pygrister/default.nix @@ -0,0 +1,55 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + requests, + typer, + pyinstaller, + tox, + nix-update-script, +}: + +buildPythonPackage (finalAttrs: { + pname = "pygrister"; + version = "0.10.0"; + pyproject = true; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "ricpol"; + repo = "pygrister"; + tag = "v${finalAttrs.version}"; + hash = "sha256-7NIiHbE9LQimStSpZrCEoWFdfpfrrS7TIctfv4y0Yqc="; + }; + + build-system = [ + setuptools + ]; + + dependencies = [ + requests + typer + ]; + + optional-dependencies = { + devel = [ + pyinstaller + tox + ]; + }; + + pythonImportsCheck = [ + "pygrister" + ]; + + passthru.updateScript = nix-update-script { }; + + meta = { + description = "Python client for the Grist API"; + homepage = "https://github.com/ricpol/pygrister"; + changelog = "https://github.com/ricpol/pygrister/releases/tag/${finalAttrs.src.tag}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ sinavir ]; + }; +}) diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index bcc29b294f5a..510791e76f84 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -15104,6 +15104,8 @@ self: super: with self; { pygrib = callPackage ../development/python-modules/pygrib { }; + pygrister = callPackage ../development/python-modules/pygrister { }; + pygrok = callPackage ../development/python-modules/pygrok { }; pygsl = callPackage ../development/python-modules/pygsl { inherit (pkgs) gsl swig; }; From 8f6784c045a82dd78231fbe95f9114857ecf0e20 Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 23 Sep 2026 01:47:02 +0200 Subject: [PATCH 4/6] grist-core: init at 1.7.19 --- pkgs/by-name/gr/grist-core/0001-nixos.patch | 88 +++++++ .../gr/grist-core/fetch-grist-enterprise.nix | 54 ++++ pkgs/by-name/gr/grist-core/package.nix | 244 ++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 pkgs/by-name/gr/grist-core/0001-nixos.patch create mode 100644 pkgs/by-name/gr/grist-core/fetch-grist-enterprise.nix create mode 100644 pkgs/by-name/gr/grist-core/package.nix diff --git a/pkgs/by-name/gr/grist-core/0001-nixos.patch b/pkgs/by-name/gr/grist-core/0001-nixos.patch new file mode 100644 index 000000000000..4e75bbd44d80 --- /dev/null +++ b/pkgs/by-name/gr/grist-core/0001-nixos.patch @@ -0,0 +1,88 @@ +diff --git a/sandbox/gvisor/run.py b/sandbox/gvisor/run.py +index db074508..d94b3cc8 100755 +--- a/sandbox/gvisor/run.py ++++ b/sandbox/gvisor/run.py +@@ -75,8 +75,8 @@ mounts = [ # These will be filled in more fully programmatically bel + binds = [] + preserved = set() + env = [ +- "PATH=/usr/local/bin:/usr/bin:/bin", +- "LD_LIBRARY_PATH=/usr/local/lib" # Assumes python version in /usr/local ++ "PATH=@sandboxPath@", ++ "LD_LIBRARY_PATH=@sandboxLibPath@", + ] + (args.env or []) + settings = { + "ociVersion": "1.0.0", +@@ -157,17 +157,13 @@ def preserve(*locations, short_failure=False): + }) + preserved.add(location) + ++ + # Prepare the file system - blank out everything that need not be shared. +-exceptions = ["/lib", "/lib64"] # to be shared (read-only) +-exceptions += ["/proc", "/sys"] # already virtualized ++exceptions = ["/proc", "/sys"] # already virtualized + + # retain /bin and /usr/bin for utilities + start = args.start +-if include_bash or start: +- exceptions.append("/bin") + +-preserve("/usr/bin") +-preserve("/usr/local/lib") + + # Support user-specific extra directories. This is handy if Python is + # somewhere weird and there is a maze of soft links to get +@@ -176,31 +172,7 @@ extra_dirs = os.environ.get('GVISOR_EXTRA_DIRS') + if extra_dirs: + preserve(*extra_dirs.split(':')) + +-# Do not attempt to include symlink directories, they are not supported +-# and will cause obscure failures. On debian bookworm /lib64 is a +-# symlink and we do not appear to need it, relative to debian buster +-# where it is a real directory. +-if os.path.exists('/lib64') and not os.path.islink('/lib64'): +- preserve("/lib64") +-if os.path.exists('/usr/lib64'): +- preserve("/usr/lib64") +-preserve("/usr/lib") +- +-# include python3 for bash and python3 +-best_python_executable = None +-# We expect python3 in /usr/bin or /usr/local/bin. +-candidates = [ +- path +- # Pick the most generic python if not matching python3.11. +- # Sorry this is delicate because of restores, mounts, symlinks. +- for pattern in ['python3.11', 'python3.10', 'python3.9', 'python3', 'python3*'] +- for root in ['/usr/local', '/usr'] +- for path in glob.glob(f'{root}/bin/{pattern}') +- if os.path.exists(path) +-] +-if not candidates: +- raise Exception('could not find python3') +-best_python_executable = os.path.realpath(candidates[0]) ++preserve("/nix/store") + + # Set up any specific shares requested. + if args.mount: +@@ -227,18 +199,15 @@ settings['mounts'] = sorted(tmpfs_mounts, key=lambda mount: mount["destination"] + # because gvisor is written in Go and doesn't use the standard library that faketime + # tweaks. + if args.faketime: +- preserve('/usr/lib/x86_64-linux-gnu/faketime') + cmd_args.append('faketime') + cmd_args.append('-f') + cmd_args.append('2020-01-01 00:00:00' if args.faketime == 'default' else args.faketime) +- preserve('/usr/bin/faketime') +- preserve('/bin/date') + + # Pick and set an initial entry point (bash or python). + if start: + cmd_args.append(start) + else: +- cmd_args.append('bash' if include_bash else best_python_executable) ++ cmd_args.append('bash' if include_bash else "python3") + + # Add any requested arguments for the program that will be run. + cmd_args += more_args diff --git a/pkgs/by-name/gr/grist-core/fetch-grist-enterprise.nix b/pkgs/by-name/gr/grist-core/fetch-grist-enterprise.nix new file mode 100644 index 000000000000..a6c481d8bdad --- /dev/null +++ b/pkgs/by-name/gr/grist-core/fetch-grist-enterprise.nix @@ -0,0 +1,54 @@ +{ + lib, + stdenv, + cacert, + curl, + + fetchYarnDeps, +}: +{ + gristSrc, + version, + hash, + offlineCacheHash, +}: +rec { + src = stdenv.mkDerivation (finalAttrs: { + pname = "grist-enterprise-src"; + inherit version; + + src = gristSrc; + + phases = [ + "buildPhase" + "installPhase" + ]; + + nativeBuildInputs = [ + curl + cacert + ]; + + buildPhase = '' + ref=$(cat $src/buildtools/.grist-ee-version) + + echo "Found grist-ee version: $ref" + + curl "https://grist-static.com/ext/ext-built-''${ref}.tar.gz" -o ./tarball.tar.gz + + ''; + installPhase = '' + mkdir -p $out + + tar -xvf ./tarball.tar.gz -C $out + ''; + + outputHash = hash; + outputHashAlgo = if finalAttrs.outputHash == "" then "sha256" else null; + outputHashMode = "recursive"; + }); + offlineCache = fetchYarnDeps { + yarnLock = "${src}/ext/yarn.lock"; + hash = offlineCacheHash; + }; +} diff --git a/pkgs/by-name/gr/grist-core/package.nix b/pkgs/by-name/gr/grist-core/package.nix new file mode 100644 index 000000000000..39f55b567324 --- /dev/null +++ b/pkgs/by-name/gr/grist-core/package.nix @@ -0,0 +1,244 @@ +{ + lib, + stdenv, + callPackage, + + fetchFromGitHub, + fetchYarnDeps, + writeScriptBin, + + python3, + yarn, + nodejs, + node-gyp-build, + node-gyp, + node-pre-gyp, + fixup-yarn-lock, + + yarnConfigHook, + + sandboxEnv ? [ ], + extraPythonPackages ? p: [ ], + + nixosTests, + + enterpriseEdition ? false, + + nix-update-script, +}: +stdenv.mkDerivation ( + finalAttrs: + let + fetchGristEnterprise = callPackage ./fetch-grist-enterprise.nix { }; + + enterprise = fetchGristEnterprise { + gristSrc = finalAttrs.src; + inherit (finalAttrs) version; + hash = "sha256-7lqt+L/Qmx/3kl2zJnRsbmCSmu+iAyzpNnqI/yJD9Q8="; + offlineCacheHash = "sha256-oMLNZZolY9Wg4DwJyYPDL6K28aEyWcXS9ivnzCuYcS0="; + }; + + pythonEnv = python3.withPackages ( + p: + extraPythonPackages p + ++ [ + p.astroid + p.asttokens + p.chardet + p.et-xmlfile + p.executing + p.friendly-traceback + p.iso8601 + p.lazy-object-proxy + p.openpyxl + p.phonenumbers + p.pure-eval + p.python-dateutil + p.roman + p.six + p.sortedcontainers + p.stack-data + p.typing-extensions + p.unittest-xml-reporting + p.wrapt + ] + ); + in + { + pname = "grist-core"; + version = "1.7.19"; + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "gristlabs"; + repo = "grist-core"; + tag = "v${finalAttrs.version}"; + hash = "sha256-IKnSluSQnPHO+qAwWF7KR/ukIDHVVulmJqIqaMI88Lg="; + }; + + offlineCache = fetchYarnDeps { + yarnLock = "${finalAttrs.src}/yarn.lock"; + hash = "sha256-E8LDY7GF9d7+iXxr4BLYRUkwQxcpVihMU6HrXJ80pic="; + }; + + env = { + # We have our own way to fetch enterprise code because of nix + GRIST_SKIP_EXT_AUTOSETUP = "1"; + + sandboxPath = lib.makeSearchPath "bin" ([ pythonEnv ] ++ sandboxEnv); + sandboxLibPath = lib.makeLibraryPath ([ pythonEnv ] ++ sandboxEnv); + }; + + strictDeps = true; + + patches = [ + # Currently, gVisor sandboxing assumes the system is following the FHS. + # We thus adapt the sandboxing for nixos by removing all useless mounts + # and adding a readonly /nix/store. In particular this implies that all + # grist users will be able to browse the nix-store. + # + # See https://github.com/gristlabs/grist-core/issues/2022 for more details + ./0001-nixos.patch + ]; + + nativeBuildInputs = [ + yarn + nodejs + fixup-yarn-lock + node-gyp-build + node-gyp + node-pre-gyp + + yarnConfigHook + + # napi-postinstall is only used for linting dependencies and/or useless checks + (writeScriptBin "napi-postinstall" '' + #!/bin/sh + # noop + echo "$@" + '') + + pythonEnv + ]; + + buildInputs = [ pythonEnv ]; + + yarnInstallFlags = [ + "--frozen-lockfile" + "--force" + "--ignore-engines" + "--ignore-platform" + # "--ignore-scripts" + "--no-progress" + "--non-interactive" + "--offline" + ]; + + postPatch = '' + rm .yarnrc + ''; + + preConfigure = '' + export HOME=$(mktemp -d) + export npm_config_nodedir=${nodejs} + + # This one is needed during install process: + patchShebangs buildtools/install_edition.sh + ''; + + preBuild = '' + patchShebangs buildtools + '' + + lib.optionalString enterpriseEdition '' + + echo "Copying ext" + cp -r --preserve=timestamps --reflink=auto -- "${enterprise.src}/ext" ./ext + chmod -R u+w -- "./ext" + + pushd ext + + yarn config --offline set yarn-offline-mirror ${enterprise.offlineCache} + fixup-yarn-lock yarn.lock + + yarn install $yarnInstallFlags \ + --production=false + + patchShebangs node_modules + + popd + + ./buildtools/dedupe-ext-types.sh + ''; + + buildPhase = '' + runHook preBuild + + yarn --offline run build:prod + + runHook postBuild + ''; + + postBuild = '' + rm -r ./node_modules + yarn install $yarnInstallFlags \ + --production=true + ''; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/grist-core" + + cp -r {_build,node_modules,sandbox,static,bower_components} "$out/grist-core" + ${lib.optionalString enterpriseEdition '' + mkdir -p "$out/grist-core/ext" + cp -r ext/assets "$out/grist-core/ext/" + cp -r ext/node_modules "$out/grist-core/ext/" + ''} + + runHook postInstall + ''; + + postInstall = '' + unlink $out/grist-core/static/mocha.js + unlink $out/grist-core/static/sinon.js + unlink $out/grist-core/static/mocha.css + + unlink $out/grist-core/node_modules/msgpackr/node_modules/.bin/download-msgpackr-prebuilds + unlink $out/grist-core/node_modules/.bin/download-msgpackr-prebuilds + + unlink $out/grist-core/bower_components/bootstrap + + substituteAllInPlace $out/grist-core/sandbox/gvisor/run.py + ''; + + passthru = { + inherit pythonEnv; + enterpriseSrc = enterprise.src; + enterpriseOfflineCache = enterprise.offlineCache; + tests.grist-core = nixosTests.grist-core; + + updateScript = nix-update-script { + extraArgs = [ + "--custom-dep" + "enterpriseSrc" + "--custom-dep" + "enterpriseOfflineCache" + ]; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ + sinavir + ]; + description = '' + Relational spreadsheet tool that sits between databases and + spreadsheets. + ''; + homepage = "https://github.com/gristlabs/grist-core"; + license = if enterpriseEdition then lib.unfree else lib.licenses.asl20; + platforms = lib.platforms.linux; + }; + } +) From 7f5817830171efb045dc979de37a0559ce89f621 Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 23 Sep 2026 01:56:22 +0200 Subject: [PATCH 5/6] nixos/grist-core: init --- .../manual/release-notes/rl-2611.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/web-apps/grist.nix | 247 ++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 nixos/modules/services/web-apps/grist.nix diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index b4f95457eef0..910b6c3032b1 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -58,6 +58,8 @@ - [snowflake-prometheus-exporter](https://github.com/grafana/snowflake-prometheus-exporter), a Prometheus exporter for Snowflake metrics. Available as [services.prometheus.exporters.snowflake](#opt-services.prometheus.exporters.snowflake.enable). +- [Grist](https://www.getgrist.com/), a powerful spreadsheet alternative. Available as [services.grist](#opt-services.grist.enable). + - [Pumpkin](https://pumpkinmc.org/), a Minecraft server implementation written entirely in Rust, focused on performance and configurability. Available as [services.pumpkin](#opt-services.pumpkin.enable). - [feishin](https://github.com/jeffvli/feishin), a modern self-hosted music player. Available as [services.feishin](#opt-services.feishin.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1ad91b265901..ad42c9606bc6 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1731,6 +1731,7 @@ ./services/web-apps/goupile.nix ./services/web-apps/grav.nix ./services/web-apps/greenlight.nix + ./services/web-apps/grist.nix ./services/web-apps/grocy.nix ./services/web-apps/guacamole-client.nix ./services/web-apps/guacamole-server.nix diff --git a/nixos/modules/services/web-apps/grist.nix b/nixos/modules/services/web-apps/grist.nix new file mode 100644 index 000000000000..fcc4e904edd0 --- /dev/null +++ b/nixos/modules/services/web-apps/grist.nix @@ -0,0 +1,247 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + concatMapStringsSep + concatStringsSep + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + optional + types + ; + + cfg = config.services.grist; +in +{ + options.services.grist = { + enable = mkEnableOption "Grist"; + + package = mkPackageOption pkgs "grist-core" { }; + + enableEnterprise = mkEnableOption "Grist enterprise code"; + + enableRedis = mkEnableOption "Grist redis data store"; + + environment = mkOption { + type = types.submodule { + freeformType = types.attrsOf (types.nullOr types.str); + + options = { + GRIST_DATA_DIR = mkOption { + type = types.path; + default = "/var/lib/grist-core/docs"; + description = '' + Directory in which to store documents. + ''; + }; + + GRIST_INST_DIR = mkOption { + type = types.path; + default = "/var/lib/grist-core"; + description = '' + Path to Grist instance configuration files, for Grist server. + ''; + }; + + GRIST_USER_ROOT = mkOption { + type = types.path; + default = "/var/lib/grist-core"; + description = '' + An extra path to look for plugins in - Grist will scan for plugins in $GRIST_USER_ROOT/plugins. + ''; + }; + + GRIST_HOST = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Address to listen on + ''; + }; + + GVISOR_FLAGS = mkOption { + type = types.listOf types.str; + default = [ + "-rootless" + ]; + apply = concatStringsSep " "; + description = '' + The flags that are passed on to gVisor when creating a sandbox. + ''; + }; + + GRIST_SANDBOX_FLAVOR = mkOption { + type = types.nullOr types.str; + default = "gvisor"; + description = '' + Sandbox to use for grist documents. Only "gvisor" is supported. + ''; + }; + + GVISOR_AVAILABLE = mkOption { + type = types.str; + default = "1"; + readOnly = true; + description = '' + Whether gvisor is available for Grist. + ''; + }; + + TYPEORM_DATABASE = mkOption { + type = types.str; + default = "/var/lib/grist-core/db.sqlite"; + description = '' + Database filename for sqlite or database name for other db types. + ''; + }; + + TYPEORM_TYPE = mkOption { + type = types.enum [ + "sqlite" + "postgres" + ]; + default = "sqlite"; + description = '' + Which database type to use for storage. + ''; + }; + GRIST_BOOT_KEY = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Grist password for first time configuration of your instance. You must remove it when your instance is configured. + + If null, this will be unset. + ''; + }; + GRIST_DEFAULT_EMAIL = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The user who logs in with the email defined by + GRIST_DEFAULT_EMAIL is the administrator of this Grist + installation. When Grist runs for the first time, it will create + an account set to the value of GRIST_DEFAULT_EMAIL. + + If null, this will be unset. + ''; + }; + }; + }; + default = { }; + example = { + GRIST_DEFAULT_EMAIL = "example@example.com"; + }; + description = '' + Environment variables used for Grist. + + See [](https://github.com/gristlabs/grist-core/#environment-variables) + for available environment variables. + ''; + }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + Environment files for secrets. + + You must at least set GRIST_SESSION_KEY there. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.environment.GRIST_SANDBOX_FLAVOR == "gvisor"; + message = "The current Grist module only supports gVisor sandboxing"; + } + ]; + + warnings = + optional (cfg.environment.GRIST_BOOT_KEY != null) + "GRIST_BOOT_KEY is world-readable in the Nix Store, you should remove it when your instance is configured or set it in the environment file as a secret."; + + services.grist = { + package = mkDefault (pkgs.grist-core.override { enterpriseEdition = cfg.enableEnterprise; }); + environment = { + REDIS_URL = mkIf cfg.enableRedis "redis://localhost:${builtins.toString config.services.redis.servers.grist.port}"; + NODE_PATH = concatMapStringsSep ":" (v: "${cfg.package}/grist-core/${v}") [ + "_build" + "_build/ext" + "_build/stubs" + "ext/node_modules" + ]; + }; + }; + + systemd.services.grist-core = { + description = "Grist Core"; + + after = [ + "network.target" + ] + ++ optional (cfg.environment.TYPEORM_TYPE == "postgres") "postgresql.service"; + + wants = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + path = [ + pkgs.nodejs + cfg.package.pythonEnv + pkgs.gvisor + pkgs.procps + pkgs.glibc.bin + ]; + + inherit (cfg) environment; + + serviceConfig = { + ExecStart = "${pkgs.nodejs}/bin/node ${cfg.package}/grist-core/_build/stubs/app/server/server.js"; + + DynamicUser = true; + Restart = "always"; + + StateDirectory = "grist-core"; + WorkingDirectory = "/var/lib/grist-core"; + + Delegate = "yes"; + + ProtectHome = true; + ProtectSystem = "strict"; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + LockPersonality = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + NoNewPrivileges = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + + EnvironmentFile = cfg.environmentFiles; + }; + }; + services.redis.servers = mkIf cfg.enableRedis { + "grist" = { + enable = true; + port = 6380; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ sinavir ]; +} From e1af7011e7cb112b50bb8c65083d34d2f7f80fd9 Mon Sep 17 00:00:00 2001 From: sinavir Date: Wed, 23 Sep 2026 15:20:25 +0200 Subject: [PATCH 6/6] nixos/tests/grist-core: init test --- nixos/tests/all-tests.nix | 1 + nixos/tests/grist.nix | 107 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 nixos/tests/grist.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9f9c1e2703cc..b1e01c128585 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -808,6 +808,7 @@ in graylog = runTest ./graylog.nix; greenlight = runTest ./greenlight.nix; greetd-no-shadow = runTest ./greetd-no-shadow.nix; + grist = runTest ./grist.nix; grocy = runTest ./grocy.nix; grow-partition = runTest ./grow-partition.nix; grub = { diff --git a/nixos/tests/grist.nix b/nixos/tests/grist.nix new file mode 100644 index 000000000000..83d2718f4972 --- /dev/null +++ b/nixos/tests/grist.nix @@ -0,0 +1,107 @@ +{ pkgs, ... }: +let + forwardedPort = 8484; + internalPort = 8484; +in +{ + name = "grist smoke test"; + + nodes = { + machine = + { pkgs, ... }: + { + virtualisation.forwardPorts = [ + { + host.port = forwardedPort; + guest.port = internalPort; + } + ]; + + networking.firewall.allowedTCPPorts = [ 8484 ]; + + services.grist = { + enable = true; + enableRedis = true; + environment = { + APP_HOME_URL = "http://127.0.0.1:8484"; + GRIST_IN_SERVICE = "true"; + GRIST_BOOT_KEY = "dummy"; + GRIST_HOST = "0.0.0.0"; + DEBUG = "1"; + }; + }; + }; + }; + + extraPythonPackages = p: [ + p.requests + ]; + + testScript = '' + import requests + import sys + + start_all() + + machine.wait_for_unit("grist-core.service") + machine.wait_for_open_port(${builtins.toString internalPort}) + + url = "http://127.0.0.1:${builtins.toString forwardedPort}/o/docs/api/" + + def check_answer(r): + if not r.ok: + sys.exit(1) + return r.json() + + with subtest("Create document"): + r = requests.post(f"{url}docs", json = {"timezone":"Europe/Paris"}) + doc_id = check_answer(r) + + with subtest("Create table"): + r = requests.post(f"{url}docs/{doc_id}/tables", json={ + "tables": [ + { + "id": "double", + "columns": [ + { + "id": "number", + "fields": { + "type": "Numeric", + "label": "Number", + }, + }, + { + "id": "result", + "fields": { + "type": "Numeric", + "label": "Result", + "formula": "$number * 2", + "isFormula": True, + }, + }, + ], + }, + ], + }) + table_id = check_answer(r)["tables"][0]["id"] + + with subtest("Add record"): + r = requests.post(f"{url}docs/{doc_id}/tables/{table_id}/records", json={ + "records": [ + { + "fields": { + "number": 5, + }, + }, + ], + }) + if len(check_answer(r)) != 1: + sys.exit(1) + + with subtest("Read record"): + r = requests.get(f"{url}docs/{doc_id}/tables/{table_id}/records") + record = check_answer(r)["records"][0] + if record["fields"]["result"] != record["fields"]["number"] * 2: + sys.exit(1) + ''; +}