Explorar o código

feat: add Grist Core self-hosted spreadsheet server

- Add grist-core package definition with Node.js/Python build
- Add NixOS module for Grist service configuration
- Include comprehensive options for port, data directory, sandboxing, and security
Zander Hawke hai 5 meses
pai
achega
e5a778d92b
Modificáronse 4 ficheiros con 286 adicións e 1 borrados
  1. 3 1
      modules/nixos/default.nix
  2. 142 0
      modules/nixos/grist.nix
  3. 1 0
      packages/default.nix
  4. 140 0
      packages/grist-core.nix

+ 3 - 1
modules/nixos/default.nix

@@ -1 +1,3 @@
-{ }
+{
+  grist = import ./grist.nix;
+}

+ 142 - 0
modules/nixos/grist.nix

@@ -0,0 +1,142 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+let
+  cfg = config.services.grist;
+in
+{
+  options.services.grist = {
+    enable = lib.mkEnableOption "Grist Core self-hosted spreadsheet server";
+
+    package = lib.mkPackageOption pkgs "grist-core" { };
+
+    port = lib.mkOption {
+      type = lib.types.port;
+      default = 8484;
+      description = "Port on which Grist listens.";
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.path;
+      default = "/var/lib/grist";
+      description = "Directory for persistent data (documents, home database, etc.). Equivalent to /persist in Docker.";
+    };
+
+    sandboxFlavor = lib.mkOption {
+      type = lib.types.enum [ "unsandboxed" "gvisor" "pyodide" "macSandboxExec" ];
+      default = "unsandboxed";
+      description = ''
+        Sandbox flavor for executing Python formulas.
+        - unsandboxed: No sandbox (fastest, least secure)
+        - gvisor: gVisor sandbox (Linux only, most secure)
+        - pyodide: WebAssembly sandbox (cross-platform, experimental)
+        - macSandboxExec: macOS native sandbox
+      '';
+    };
+
+    singleOrg = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = null;
+      example = "mycompany";
+      description = "If set, pins Grist to a single organization (simplifies UI, common for self-hosted).";
+    };
+
+    defaultEmail = lib.mkOption {
+      type = lib.types.str;
+      default = "[email protected]";
+      description = "Default admin email used for initial login.";
+    };
+
+    host = lib.mkOption {
+      type = lib.types.str;
+      default = "0.0.0.0";
+      description = "Host interface to bind (0.0.0.0 for all interfaces).";
+    };
+
+    orgInPath = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = "Include organization in URL path (Docker default).";
+    };
+
+    serveSameOrigin = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = "Serve home and docs on the same origin/port (Docker default).";
+    };
+
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = "Open the Grist port in the firewall.";
+    };
+
+    environment = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      description = "Additional environment variables to pass to Grist.";
+    };
+
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = "File containing environment variables (e.g., for secrets like GRIST_SESSION_SECRET).";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.grist = {
+      description = "Grist Core Spreadsheet Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      environment = {
+        PORT = toString cfg.port;
+        GRIST_HOST = cfg.host;
+        GRIST_SINGLE_PORT = if cfg.serveSameOrigin then "true" else "false";
+        GRIST_ORG_IN_PATH = if cfg.orgInPath then "true" else "false";
+        GRIST_DATA_DIR = "${cfg.dataDir}/docs";
+        GRIST_INST_DIR = cfg.dataDir;
+        GRIST_SESSION_COOKIE = "grist_core";
+        GRIST_SANDBOX_FLAVOR = cfg.sandboxFlavor;
+        NODE_OPTIONS = "--no-deprecation";
+        NODE_ENV = "production";
+        TYPEORM_DATABASE = "${cfg.dataDir}/home.sqlite3";
+      } // lib.optionalAttrs (cfg.singleOrg != null) {
+        GRIST_SINGLE_ORG = cfg.singleOrg;
+        GRIST_DEFAULT_EMAIL = cfg.defaultEmail;
+      } // cfg.environment;
+
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package}";
+        DynamicUser = true;
+        StateDirectory = "grist";
+        StateDirectoryMode = "0700";
+        WorkingDirectory = cfg.dataDir;
+        EnvironmentFile = lib.optionalString (cfg.environmentFile != null) cfg.environmentFile;
+        Restart = "always";
+        # Hardening
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        NoNewPrivileges = true;
+        RestrictSUIDSGID = true;
+      };
+
+      preStart = ''
+        # Ensure data directories exist with correct ownership
+        mkdir -p ${cfg.dataDir}/docs
+        chown -R $USER:$GROUP ${cfg.dataDir}
+      '';
+    };
+
+    networking.firewall = lib.mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    # Optional: expose the package for passthru.tests or manual use
+    environment.systemPackages = [ cfg.package ];
+  };
+}

+ 1 - 0
packages/default.nix

@@ -14,6 +14,7 @@
 {
   aerospace-tmux-focus = pkgs.callPackage ./aerospace-tmux-focus.nix { };
   default = pkgs.callPackage ./quick-setup.nix { };
+  grist-core = pkgs.callPackage ./grist-core.nix { };
   go-avahi-cname = pkgs.callPackage ./go-avahi-cname.nix { };
   hello = pkgs.callPackage ./hello.nix { };
   nct6775-pwm-udev-package = pkgs.callPackage ./nct6775-pwm-udev-package.nix { };

+ 140 - 0
packages/grist-core.nix

@@ -0,0 +1,140 @@
+{ lib
+, stdenv
+, fetchFromGitHub
+, python3
+, fetchYarnDeps
+, yarn
+, nodejs
+, prefetch-yarn-deps
+, fixup-yarn-lock
+, makeWrapper
+, gitUpdater
+, pkg-config
+, sqlite
+, apple-sdk
+, cctools
+, cacert
+, gvisor
+,
+}:
+stdenv.mkDerivation rec {
+  pname = "grist-core";
+  version = "1.7.8";
+
+  src = fetchFromGitHub {
+    owner = "gristlabs";
+    repo = "grist-core";
+    tag = "v${version}";
+    hash = "sha256-e68PtWdlgKdjCj97fp+Rh3WwwwIcvKSXlx4Ry1qUeLg=";
+  };
+
+  offlineCache = fetchYarnDeps {
+    yarnLock = "${src}/yarn.lock";
+    hash = "sha256-7zyuBxheftgCXGjjJ+rdwSslIro9IEd/uvmo4xp6I+Q=";
+  };
+
+  nativeBuildInputs = [
+    yarn
+    nodejs
+    prefetch-yarn-deps
+    fixup-yarn-lock
+    makeWrapper
+    pkg-config
+    python3
+  ] ++ lib.optionals stdenv.hostPlatform.isDarwin [
+    cctools
+  ];
+
+  buildInputs = [
+    sqlite
+  ] ++ lib.optionals stdenv.hostPlatform.isDarwin [
+    apple-sdk
+  ] ++ lib.optionals stdenv.hostPlatform.isLinux [
+    gvisor
+  ];
+
+  passthru = {
+    updateScript = gitUpdater { rev-prefix = "v"; };
+    tests = { };
+  };
+
+  configurePhase = ''
+    runHook preConfigure
+
+    export HOME=$(mktemp -d)
+
+    rm .yarnrc
+
+    yarn config --offline set yarn-offline-mirror ${offlineCache}
+    fixup-yarn-lock yarn.lock
+
+    mkdir -p "$HOME/.node-gyp/${nodejs.version}"
+    echo 9 >"$HOME/.node-gyp/${nodejs.version}/installVersion"
+    ln -sfv "${nodejs}/include" "$HOME/.node-gyp/${nodejs.version}"
+    export npm_config_nodedir=${nodejs}
+
+    # Set SSL certificates for node-pre-gyp
+    export SSL_CERT_FILE="${cacert}/etc/ssl/certs/ca-bundle.crt"
+    export NODE_EXTRA_CA_CERTS="${cacert}/etc/ssl/certs/ca-bundle.crt"
+
+    # Ensure cctools' libtool comes before any other libtool
+    ${lib.optionalString stdenv.hostPlatform.isDarwin ''
+      export PATH="${cctools}/bin:$PATH"
+    ''}
+
+    yarn --offline --frozen-lockfile --ignore-platform --ignore-engines --ignore-optional --no-progress --non-interactive install
+    yarn install:python
+
+    patchShebangs node_modules
+    patchShebangs buildtools
+
+    runHook postConfigure
+  '';
+
+  buildPhase = ''
+    runHook preBuild
+
+    yarn --offline run build:prod
+
+    runHook postBuild
+  '';
+
+  installPhase = ''
+    runHook preInstall
+
+    mkdir -p "$out/libexec" "$out/bin"
+
+    # Copy runtime files + the sandbox venv
+    cp -r _build node_modules plugins sandbox static bower_components package.json sandbox_venv3 $out/libexec/
+
+    makeWrapper ${lib.getExe nodejs} $out/bin/grist-core \
+      --add-flags "$out/libexec/_build/stubs/app/server/server.js" \
+      --set "GRIST_PYTHON_VIRTUALENV" "$out/libexec/sandbox_venv3" \
+      --set "NODE_PATH" "$out/libexec/_build:$out/libexec/_build/stubs:$out/libexec/_build/ext" \
+      --chdir "$out/libexec"
+
+    runHook postInstall
+  '';
+
+  postInstall = ''
+    # Remove test/dev files that might not exist
+    rm -f $out/libexec/static/mocha.js
+    rm -f $out/libexec/static/sinon.js
+    rm -f $out/libexec/static/mocha.css
+    
+    # Fix or remove broken symlinks
+    find $out/libexec/bower_components -type l ! -exec test -e {} \; -delete
+    
+    # Remove problematic .bin directories
+    find $out/libexec/node_modules -name '.bin' -type d -print0 | xargs -0 rm -rf 2>/dev/null || true
+  '';
+
+  meta = {
+    description = "Grist is the evolution of spreadsheets";
+    homepage = "https://github.com/gristlabs/grist-core";
+    license = lib.licenses.asl20;
+    maintainers = with lib.maintainers; [ scandiravian ];
+    mainProgram = "grist-core";
+    platforms = lib.platforms.unix;
+  };
+}