grist-core: init at 1.7.19 (#566754)

This commit is contained in:
Ryan Lahfa
2026-09-25 20:11:20 +00:00
committed by GitHub
12 changed files with 857 additions and 0 deletions

View File

@@ -26943,6 +26943,12 @@
github = "sinanmohd";
githubId = 69694713;
};
sinavir = {
name = "Sinavir";
email = "sinavir@sinavir.fr";
github = "sinavir";
githubId = 36380103;
};
sinics = {
name = "Zhifan";
email = "nonno.felice69uwu@gmail.com";

View File

@@ -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).

View File

@@ -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

View File

@@ -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 ];
}

View File

@@ -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 = {

107
nixos/tests/grist.nix Normal file
View File

@@ -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)
'';
}

View File

@@ -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

View File

@@ -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;
};
}

View File

@@ -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;
};
}
)

View File

@@ -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 ];
};
}

View File

@@ -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 ];
};
})

View File

@@ -6522,6 +6522,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 { };
@@ -15112,6 +15114,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; };