{ stdenv, pkgs, makeWrapper, runCommand, lib, writeShellScript, fetchFromGitHub, bundlerEnv, callPackage, ruby_3_2, replace, gzip, gnutar, git, cacert, util-linux, gawk, nettools, imagemagick, optipng, pngquant, libjpeg, jpegoptim, gifsicle, jhead, oxipng, libpsl, redis, postgresql, which, brotli, procps, rsync, icu, fetchYarnDeps, yarn, fixup-yarn-lock, nodePackages, nodejs_18, jq, moreutils, terser, uglify-js, plugins ? [ ], }@args: let version = "3.3.2"; src = fetchFromGitHub { owner = "discourse"; repo = "discourse"; rev = "v${version}"; sha256 = "sha256-FaPcUta5z/8oasw+9zGBRZnUVYD8eCo1t/XwwsFoSM8="; }; ruby = ruby_3_2; runtimeDeps = [ # For backups, themes and assets rubyEnv.wrappedRuby rsync gzip gnutar git brotli nodejs_18 # Misc required system utils which procps # For ps and kill util-linux # For renice gawk nettools # For hostname # Image optimization imagemagick optipng oxipng pngquant libjpeg jpegoptim gifsicle nodePackages.svgo jhead ]; runtimeEnv = { HOME = "/run/discourse/home"; RAILS_ENV = "production"; UNICORN_LISTENER = "/run/discourse/sockets/unicorn.sock"; }; mkDiscoursePlugin = { name ? null, pname ? null, version ? null, meta ? null, bundlerEnvArgs ? { }, preserveGemsDir ? false, src, ... }@args: let rubyEnv = bundlerEnv ( bundlerEnvArgs // { inherit name pname version ruby ; } ); in stdenv.mkDerivation ( builtins.removeAttrs args [ "bundlerEnvArgs" ] // { pluginName = if name != null then name else "${pname}-${version}"; dontConfigure = true; dontBuild = true; installPhase = '' runHook preInstall mkdir -p $out cp -r * $out/ '' + lib.optionalString (bundlerEnvArgs != { }) ( if preserveGemsDir then '' cp -r ${rubyEnv}/lib/ruby/gems/* $out/gems/ '' else '' if [[ -e $out/gems ]]; then echo "Warning: The repo contains a 'gems' directory which will be removed!" echo " If you need to preserve it, set 'preserveGemsDir = true'." rm -r $out/gems fi ln -sf ${rubyEnv}/lib/ruby/gems $out/gems '' + '' runHook postInstall '' ); } ); rake = runCommand "discourse-rake" { nativeBuildInputs = [ makeWrapper ]; } '' mkdir -p $out/bin makeWrapper ${rubyEnv}/bin/rake $out/bin/discourse-rake \ ${ lib.concatStrings (lib.mapAttrsToList (name: value: "--set ${name} '${value}' ") runtimeEnv) } \ --prefix PATH : ${lib.makeBinPath runtimeDeps} \ --set RAKEOPT '-f ${discourse}/share/discourse/Rakefile' \ --chdir '${discourse}/share/discourse' ''; rubyEnv = bundlerEnv { name = "discourse-ruby-env-${version}"; inherit version ruby; gemdir = ./rubyEnv; gemset = let gems = import ./rubyEnv/gemset.nix; in gems // { mini_racer = gems.mini_racer // { buildInputs = [ icu ]; dontBuild = false; NIX_LDFLAGS = "-licui18n"; }; libv8-node = let noopScript = writeShellScript "noop" "exit 0"; linkFiles = writeShellScript "link-files" '' cd ../.. mkdir -p vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/ ln -s "${nodejs_18.libv8}/lib/libv8.a" vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/libv8_monolith.a ln -s ${nodejs_18.libv8}/include vendor/v8/include mkdir -p ext/libv8-node echo '--- !ruby/object:Libv8::Node::Location::Vendor {}' >ext/libv8-node/.location.yml ''; in gems.libv8-node // { dontBuild = false; postPatch = '' cp ${noopScript} libexec/build-libv8 cp ${noopScript} libexec/build-monolith cp ${noopScript} libexec/download-node cp ${noopScript} libexec/extract-node cp ${linkFiles} libexec/inject-libv8 ''; }; mini_suffix = gems.mini_suffix // { propagatedBuildInputs = [ libpsl ]; dontBuild = false; # Use our libpsl instead of the vendored one, which isn't # available for aarch64. It has to be called # libpsl.x86_64.so or it isn't found. postPatch = '' cp $(readlink -f ${lib.getLib libpsl}/lib/libpsl.so) vendor/libpsl.x86_64.so ''; }; }; groups = [ "default" "assets" "development" "test" ]; }; assets = stdenv.mkDerivation { pname = "discourse-assets"; inherit version src; yarnOfflineCache = fetchYarnDeps { yarnLock = src + "/yarn.lock"; hash = "sha256-cSQofaULCmPuWGxS+hK4KlRq9lSkCPiYvhax9X6Dor8="; }; nativeBuildInputs = runtimeDeps ++ [ postgresql redis uglify-js terser yarn jq moreutils fixup-yarn-lock ]; outputs = [ "out" "javascripts" "node_modules" ]; patches = [ # Use the Ruby API version in the plugin gem path, to match the # one constructed by bundlerEnv ./plugin_gem_api_version.patch # Change the path to the auto generated plugin assets, which # defaults to the plugin's directory and isn't writable at the # time of asset generation ./auto_generated_path.patch # Fix the rake command used to recursively execute itself in the # assets precompilation task. ./assets_rake_command.patch # Little does he know, so he decided there is no need to generate the # theme-transpiler over and over again. Which at the same time allows the removal # of javascript devDependencies from the runtime environment. ./prebuild-theme-transpiler.patch ]; env.RAILS_ENV = "production"; # We have to set up an environment that is close enough to # production ready or the assets:precompile task refuses to # run. This means that Redis and PostgreSQL has to be running and # database migrations performed. preBuild = '' # Yarn wants a real home directory to write cache, config, etc to export HOME=$NIX_BUILD_TOP/fake_home yarn_install() { local offlineCache=$1 yarnLock=$2 # Make yarn install packages from our offline cache, not the registry yarn config --offline set yarn-offline-mirror $offlineCache # Fixup "resolved"-entries in yarn.lock to match our offline cache fixup-yarn-lock $yarnLock # Install while ignoring hook scripts yarn --offline --ignore-scripts --cwd $(dirname $yarnLock) install } # Install runtime and devDependencies. # The dev deps are necessary for generating the theme-transpiler executed as dependent task # assets:precompile:theme_transpiler before db:migrate and unfortunately also in the runtime yarn_install $yarnOfflineCache yarn.lock # Patch before running postinstall hook script patchShebangs node_modules/ patchShebangs --build app/assets/javascripts yarn --offline --cwd app/assets/javascripts run postinstall export SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt redis-server >/dev/null & initdb -A trust $NIX_BUILD_TOP/postgres >/dev/null postgres -D $NIX_BUILD_TOP/postgres -k $NIX_BUILD_TOP >/dev/null & export PGHOST=$NIX_BUILD_TOP echo "Waiting for Redis and PostgreSQL to be ready.." while ! redis-cli --scan >/dev/null || ! psql -l >/dev/null; do sleep 0.1 done psql -d postgres -tAc 'CREATE USER "discourse"' psql -d postgres -tAc 'CREATE DATABASE "discourse" OWNER "discourse"' psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm" psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS hstore" ${lib.concatMapStringsSep "\n" (p: "ln -sf ${p} plugins/${p.pluginName or ""}") plugins} bundle exec rake db:migrate >/dev/null chmod -R +w tmp ''; buildPhase = '' runHook preBuild bundle exec rake assets:precompile runHook postBuild ''; installPhase = '' runHook preInstall mv public/assets $out mv node_modules $node_modules rm -r app/assets/javascripts/plugins mv app/assets/javascripts $javascripts ln -sf /run/discourse/assets/javascripts/plugins $javascripts/plugins runHook postInstall ''; }; discourse = stdenv.mkDerivation { pname = "discourse"; inherit version src; buildInputs = [ rubyEnv rubyEnv.wrappedRuby rubyEnv.bundler ]; patches = [ # Load a separate NixOS site settings file ./nixos_defaults.patch # Add a noninteractive admin creation task ./admin_create.patch # Add the path to the CA cert bundle to make TLS work ./action_mailer_ca_cert.patch # Log Unicorn messages to the journal and make request timeout # configurable ./unicorn_logging_and_timeout.patch # Use the Ruby API version in the plugin gem path, to match the # one constructed by bundlerEnv ./plugin_gem_api_version.patch # Change the path to the auto generated plugin assets, which # defaults to the plugin's directory and isn't writable at the # time of asset generation ./auto_generated_path.patch # Make sure the notification email setting applies ./notification_email.patch # Little does he know, so he decided there is no need to generate the # theme-transpiler over and over again. Which at the same time allows the removal # of javascript devDependencies from the runtime environment. ./prebuild-theme-transpiler.patch ]; postPatch = '' # Always require lib-files and application.rb through their store # path, not their relative state directory path. This gets rid of # warnings and means we don't have to link back to lib from the # state directory. find config -type f -name "*.rb" -execdir \ sed -Ei "s,(\.\./)+(lib|app)/,$out/share/discourse/\2/," {} \; find config -maxdepth 1 -type f -name "*.rb" -execdir \ sed -Ei "s,require_relative (\"|')([[:alnum:]].*)(\"|'),require_relative '$out/share/discourse/config/\2'," {} \; ''; buildPhase = '' runHook preBuild mv config config.dist mv public public.dist runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out/share cp -r . $out/share/discourse rm -r $out/share/discourse/log ln -sf /var/log/discourse $out/share/discourse/log ln -sf /var/lib/discourse/tmp $out/share/discourse/tmp ln -sf /run/discourse/config $out/share/discourse/config ln -sf /run/discourse/public $out/share/discourse/public # This needs to be copied because of symlinks in node_modules # Also this needs to be full node_modules (including dev deps) because without loader.js it just throws 500 cp -r ${assets.node_modules} $out/share/discourse/node_modules ln -sf ${assets} $out/share/discourse/public.dist/assets rm -r $out/share/discourse/app/assets/javascripts ln -sf ${assets.javascripts} $out/share/discourse/app/assets/javascripts ${lib.concatMapStringsSep "\n" ( p: "ln -sf ${p} $out/share/discourse/plugins/${p.pluginName or ""}" ) plugins} runHook postInstall ''; meta = with lib; { homepage = "https://www.discourse.org/"; platforms = platforms.linux; maintainers = with maintainers; [ talyz ]; license = licenses.gpl2Plus; description = "Discourse is an open source discussion platform"; }; passthru = { inherit rubyEnv runtimeEnv runtimeDeps rake mkDiscoursePlugin assets ; inherit (pkgs) discourseAllPlugins ; enabledPlugins = plugins; plugins = callPackage ./plugins/all-plugins.nix { inherit mkDiscoursePlugin; }; ruby = rubyEnv.wrappedRuby; tests = import ../../../../nixos/tests/discourse.nix { inherit (stdenv) system; inherit pkgs; package = pkgs.discourse.override args; }; }; }; in discourse