Files
nixpkgs/pkgs/servers/web-apps/discourse/default.nix

546 lines
14 KiB
Nix

{
stdenv,
pkgs,
makeWrapper,
runCommand,
lib,
writeShellScript,
fetchFromGitHub,
bundlerEnv,
callPackage,
nixosTests,
defaultGemConfig,
ruby_3_3,
gzip,
gnutar,
git,
cacert,
util-linux,
gawk,
net-tools,
imagemagick,
optipng,
pngquant,
libjpeg,
jpegoptim,
gifsicle,
jhead,
oxipng,
libpsl,
redis,
postgresql,
which,
brotli,
procps,
rsync,
icu,
rustPlatform,
buildRubyGem,
rustc,
cargo,
pnpm_9,
fetchPnpmDeps,
pnpmConfigHook,
svgo,
nodejs,
jq,
moreutils,
terser,
uglify-js,
plugins ? [ ],
}:
let
version = "3.5.3";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse";
rev = "v${version}";
sha256 = "sha256-2lx6vFxio2CkMWa0vmzUGTljz1WC9OzpNgSxKjYPn8g=";
};
ruby = ruby_3_3;
runtimeDeps = [
# For backups, themes and assets
rubyEnv.wrappedRuby
rsync
gzip
gnutar
git
brotli
nodejs
# Misc required system utils
which
procps # For ps and kill
util-linux # For renice
gawk
net-tools # For hostname
# Image optimization
imagemagick
optipng
oxipng
pngquant
libjpeg
jpegoptim
gifsicle
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 (
# Allow overriding the plugin name
{
pluginName = if name != null then name else "${pname}-${version}";
}
// removeAttrs args [ "bundlerEnvArgs" ]
// {
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 = import ./rubyEnv/gemset.nix;
gemConfig = defaultGemConfig // {
mini_racer = attrs: {
buildInputs = [ icu ];
dontBuild = false;
NIX_LDFLAGS = "-licui18n";
};
libv8-node =
attrs:
let
noopScript = writeShellScript "noop" "exit 0";
linkFiles = writeShellScript "link-files" ''
cd ../..
mkdir -p vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/
ln -s "${nodejs.libv8}/lib/libv8.a" vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/libv8_monolith.a
ln -s ${nodejs.libv8}/include vendor/v8/include
mkdir -p ext/libv8-node
echo '--- !ruby/object:Libv8::Node::Location::Vendor {}' >ext/libv8-node/.location.yml
'';
in
{
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 = attrs: {
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
'';
};
tokenizers = attrs: {
cargoDeps = rustPlatform.fetchCargoVendor {
inherit (buildRubyGem { inherit (attrs) gemName version source; })
name
src
unpackPhase
nativeBuildInputs
;
hash = "sha256-ydSXo3wp13/mPgJv1HbavNurkd2KxuKzuJNHliPpn2I=";
};
dontBuild = false;
nativeBuildInputs = [
cargo
rustc
rustPlatform.cargoSetupHook
rustPlatform.bindgenHook
];
disallowedReferences = [
rustc.unwrapped
];
preInstall = ''
export CARGO_HOME="$PWD/../.cargo/"
'';
postInstall = ''
find $out -type f -name .rustc_info.json -delete
'';
};
tiktoken_ruby = attrs: {
cargoDeps = rustPlatform.fetchCargoVendor {
inherit (buildRubyGem { inherit (attrs) gemName version source; })
name
src
unpackPhase
nativeBuildInputs
;
hash = "sha256-IABOxUymtFkF9sl1kRWAS5hM6GNJI6Y4VFICXdX7zF0=";
};
dontBuild = false;
nativeBuildInputs = [
cargo
rustc
rustPlatform.cargoSetupHook
rustPlatform.bindgenHook
];
disallowedReferences = [
rustc.unwrapped
];
preInstall = ''
export CARGO_HOME="$PWD/../.cargo/"
'';
postInstall = ''
#ls $GEM_HOME/gems/${attrs.gemName}-${attrs.version}/lib
#mv -v $GEM_HOME/gems/${attrs.gemName}-${attrs.version}/lib/{glfm_markdown/glfm_markdown.so,}
find $out -type f -name .rustc_info.json -delete
'';
};
};
groups = [
"default"
"assets"
"development"
"test"
];
};
assets = stdenv.mkDerivation {
pname = "discourse-assets";
inherit version src;
pnpmDeps = fetchPnpmDeps {
pname = "discourse-assets";
inherit version src;
pnpm = pnpm_9;
fetcherVersion = 1;
hash = "sha256-npRKX5Lr2QrPD8OFBysDl30exP+FTnjMxFeR/Gv0Z0I=";
};
nativeBuildInputs = runtimeDeps ++ [
(postgresql.withPackages (ps: [
ps.pgvector
]))
redis
uglify-js
terser
jq
moreutils
nodejs
pnpmConfigHook
pnpm_9
];
outputs = [
"out"
"javascripts"
"node_modules"
"generated"
];
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";
env.DISCOURSE_DOWNLOAD_PRE_BUILT_ASSETS = "0";
# Allow to use different bundler version than the lockfile has
env.BUNDLER_VERSION = pkgs.bundler.version;
# requires full git and repository, even a src `leaveDotGit` is not enough. So patch this function to return the version
postPatch = ''
substituteInPlace script/assemble_ember_build.rb --replace-fail "def core_tree_hash" "def core_tree_hash; return \"v${version}\""
'';
# 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 = ''
# Patch before running postinstall hook script
patchShebangs node_modules/
patchShebangs --build app/assets/javascripts
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"
psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS vector"
${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
patchShebangs script/
bundle exec rake assets:precompile
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv public/assets $out
mv node_modules $node_modules
rm -rf app/assets/javascripts/plugins
mv app/assets/javascripts $javascripts
ln -sf /run/discourse/assets/javascripts/plugins $javascripts/plugins
mv app/assets/generated $generated
runHook postInstall
'';
# The node_modules output by design has broken symlinks, as it refers to the source code.
# They are resolved in the primary discourse derivation.
dontCheckForBrokenSymlinks = true;
};
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
# Our app/assets/generated folder is a symlink, but the ruby File.mkdir_p doesn't allow
# a symlink in the way to the last directory. This patch explicitly resolves the symlink.
./resolve_generated_assets_symlink.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
ln -sf /run/discourse/assets-generated $out/share/discourse/app/assets/generated
ln -sf ${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
# This needs to be copied because it contains symlinks to node_modules
cp -r ${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
'';
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 = {
inherit (nixosTests)
discourse
discourseAllPlugins
;
};
};
meta = {
homepage = "https://www.discourse.org/";
platforms = lib.platforms.linux;
maintainers = with lib.maintainers; [ talyz ];
license = lib.licenses.gpl2Plus;
description = "Open source discussion platform";
};
};
in
discourse