nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
1{
2 stdenv,
3 pkgs,
4 makeWrapper,
5 runCommand,
6 lib,
7 writeShellScript,
8 fetchFromGitHub,
9 bundlerEnv,
10 callPackage,
11 nixosTests,
12
13 defaultGemConfig,
14 ruby_3_3,
15 gzip,
16 gnutar,
17 git,
18 cacert,
19 util-linux,
20 gawk,
21 net-tools,
22 imagemagick,
23 optipng,
24 pngquant,
25 libjpeg,
26 jpegoptim,
27 gifsicle,
28 jhead,
29 oxipng,
30 libpsl,
31 redis,
32 postgresql,
33 which,
34 brotli,
35 procps,
36 rsync,
37 icu,
38 rustPlatform,
39 buildRubyGem,
40 rustc,
41 cargo,
42 pnpm_9,
43 fetchPnpmDeps,
44 pnpmConfigHook,
45 svgo,
46 nodejs_22,
47 jq,
48 moreutils,
49 terser,
50 uglify-js,
51
52 plugins ? [ ],
53}:
54
55let
56 version = "2025.12.0";
57
58 src = fetchFromGitHub {
59 owner = "discourse";
60 repo = "discourse";
61 rev = "v${version}";
62 sha256 = "sha256-zURRueaYWl1FqOI12H+S/ssMhN38nDuWRBqb579w6HQ=";
63 };
64
65 ruby = ruby_3_3;
66
67 runtimeDeps = [
68 # For backups, themes and assets
69 rubyEnv.wrappedRuby
70 rsync
71 gzip
72 gnutar
73 git
74 brotli
75 nodejs_22
76
77 # Misc required system utils
78 which
79 procps # For ps and kill
80 util-linux # For renice
81 gawk
82 net-tools # For hostname
83
84 # Image optimization
85 imagemagick
86 optipng
87 oxipng
88 pngquant
89 libjpeg
90 jpegoptim
91 gifsicle
92 svgo
93 jhead
94 ];
95
96 runtimeEnv = {
97 HOME = "/run/discourse/home";
98 RAILS_ENV = "production";
99 UNICORN_LISTENER = "/run/discourse/sockets/unicorn.sock";
100 };
101
102 mkDiscoursePlugin =
103 {
104 name ? null,
105 pname ? null,
106 version ? null,
107 meta ? null,
108 bundlerEnvArgs ? { },
109 preserveGemsDir ? false,
110 src,
111 ...
112 }@args:
113 let
114 rubyEnv = bundlerEnv (
115 bundlerEnvArgs
116 // {
117 inherit
118 name
119 pname
120 version
121 ruby
122 ;
123 }
124 );
125 in
126 stdenv.mkDerivation (
127 # Allow overriding the plugin name
128 {
129 pluginName = if name != null then name else "${pname}-${version}";
130 }
131 // removeAttrs args [ "bundlerEnvArgs" ]
132 // {
133 dontConfigure = true;
134 dontBuild = true;
135 installPhase = ''
136 runHook preInstall
137 mkdir -p $out
138 cp -r * $out/
139 ''
140 + lib.optionalString (bundlerEnvArgs != { }) (
141 if preserveGemsDir then
142 ''
143 cp -r ${rubyEnv}/lib/ruby/gems/* $out/gems/
144 ''
145 else
146 ''
147 if [[ -e $out/gems ]]; then
148 echo "Warning: The repo contains a 'gems' directory which will be removed!"
149 echo " If you need to preserve it, set 'preserveGemsDir = true'."
150 rm -r $out/gems
151 fi
152 ln -sf ${rubyEnv}/lib/ruby/gems $out/gems
153 ''
154 + ''
155 runHook postInstall
156 ''
157 );
158 }
159 );
160
161 rake =
162 runCommand "discourse-rake"
163 {
164 nativeBuildInputs = [ makeWrapper ];
165 }
166 ''
167 mkdir -p $out/bin
168 makeWrapper ${rubyEnv}/bin/rake $out/bin/discourse-rake \
169 ${
170 lib.concatStrings (lib.mapAttrsToList (name: value: "--set ${name} '${value}' ") runtimeEnv)
171 } \
172 --prefix PATH : ${lib.makeBinPath runtimeDeps} \
173 --set RAKEOPT '-f ${discourse}/share/discourse/Rakefile' \
174 --chdir '${discourse}/share/discourse'
175 '';
176
177 rubyEnv = bundlerEnv {
178 name = "discourse-ruby-env-${version}";
179 inherit version ruby;
180 gemdir = ./rubyEnv;
181 gemset = import ./rubyEnv/gemset.nix;
182 gemConfig = defaultGemConfig // {
183 mini_racer = attrs: {
184 buildInputs = [ icu ];
185 dontBuild = false;
186 NIX_LDFLAGS = "-licui18n";
187 };
188 libv8-node =
189 attrs:
190 let
191 noopScript = writeShellScript "noop" "exit 0";
192 linkFiles = writeShellScript "link-files" ''
193 cd ../..
194
195 mkdir -p vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/
196 ln -s "${nodejs_22.libv8}/lib/libv8.a" vendor/v8/${stdenv.hostPlatform.system}/libv8/obj/libv8_monolith.a
197
198 ln -s ${nodejs_22.libv8}/include vendor/v8/include
199
200 mkdir -p ext/libv8-node
201 echo '--- !ruby/object:Libv8::Node::Location::Vendor {}' >ext/libv8-node/.location.yml
202 '';
203 in
204 {
205 dontBuild = false;
206 postPatch = ''
207 cp ${noopScript} libexec/build-libv8
208 cp ${noopScript} libexec/build-monolith
209 cp ${noopScript} libexec/download-node
210 cp ${noopScript} libexec/extract-node
211 cp ${linkFiles} libexec/inject-libv8
212 '';
213 };
214 mini_suffix = attrs: {
215 propagatedBuildInputs = [ libpsl ];
216 dontBuild = false;
217 # Use our libpsl instead of the vendored one, which isn't
218 # available for aarch64. It has to be called
219 # libpsl.x86_64.so or it isn't found.
220 postPatch = ''
221 cp $(readlink -f ${lib.getLib libpsl}/lib/libpsl.so) vendor/libpsl.x86_64.so
222 '';
223 };
224 tokenizers = attrs: {
225 cargoDeps = rustPlatform.fetchCargoVendor {
226 inherit (buildRubyGem { inherit (attrs) gemName version source; })
227 name
228 src
229 unpackPhase
230 nativeBuildInputs
231 ;
232 hash = "sha256-Yxcerq4Wil1nrEzHoEmsTAj4VnUmrwRlA3WO2b72yOc=";
233 };
234
235 dontBuild = false;
236
237 nativeBuildInputs = [
238 cargo
239 rustc
240 rustPlatform.cargoSetupHook
241 rustPlatform.bindgenHook
242 ];
243
244 disallowedReferences = [
245 rustc.unwrapped
246 ];
247
248 preInstall = ''
249 export CARGO_HOME="$PWD/../.cargo/"
250 '';
251
252 postInstall = ''
253 find $out -type f -name .rustc_info.json -delete
254 '';
255 };
256 tiktoken_ruby = attrs: {
257 cargoDeps = rustPlatform.fetchCargoVendor {
258 inherit (buildRubyGem { inherit (attrs) gemName version source; })
259 name
260 src
261 unpackPhase
262 nativeBuildInputs
263 ;
264 hash = "sha256-zyGK+XJpMls6w0Uydegqsj4TH4IrVxANm0qmpR7+95I=";
265 };
266
267 dontBuild = false;
268
269 nativeBuildInputs = [
270 cargo
271 rustc
272 rustPlatform.cargoSetupHook
273 rustPlatform.bindgenHook
274 ];
275
276 disallowedReferences = [
277 rustc.unwrapped
278 ];
279
280 preInstall = ''
281 export CARGO_HOME="$PWD/../.cargo/"
282 '';
283
284 postInstall = ''
285 #ls $GEM_HOME/gems/${attrs.gemName}-${attrs.version}/lib
286 #mv -v $GEM_HOME/gems/${attrs.gemName}-${attrs.version}/lib/{glfm_markdown/glfm_markdown.so,}
287 find $out -type f -name .rustc_info.json -delete
288 '';
289 };
290 };
291
292 groups = [
293 "default"
294 "assets"
295 "development"
296 "test"
297 ];
298 };
299
300 assets = stdenv.mkDerivation {
301 pname = "discourse-assets";
302 inherit version src;
303
304 pnpmDeps = fetchPnpmDeps {
305 pname = "discourse-assets";
306 inherit version src;
307 pnpm = pnpm_9;
308 fetcherVersion = 1;
309 hash = "sha256-/GJQqbmBXn5SSdxQ3TBQEUGe6Qm7aJ1ogoYqOFD5Pm0=";
310 };
311
312 nativeBuildInputs = runtimeDeps ++ [
313 (postgresql.withPackages (ps: [
314 ps.pgvector
315 ]))
316 redis
317 uglify-js
318 terser
319 jq
320 moreutils
321 nodejs_22
322 pnpmConfigHook
323 pnpm_9
324 ];
325
326 outputs = [
327 "out"
328 "node_modules"
329 "generated"
330 "frontend"
331 ];
332
333 patches = [
334 # Use the Ruby API version in the plugin gem path, to match the
335 # one constructed by bundlerEnv
336 ./plugin_gem_api_version.patch
337
338 # Change the path to the auto generated plugin assets, which
339 # defaults to the plugin's directory and isn't writable at the
340 # time of asset generation
341 ./auto_generated_path.patch
342
343 # Fix the rake command used to recursively execute itself in the
344 # assets precompilation task.
345 ./assets_rake_command.patch
346
347 # Little does he know, so he decided there is no need to generate the
348 # theme-transpiler over and over again. Which at the same time allows the removal
349 # of javascript devDependencies from the runtime environment.
350 ./prebuild-asset-processor.patch
351 ];
352
353 env.RAILS_ENV = "production";
354 env.DISCOURSE_DOWNLOAD_PRE_BUILT_ASSETS = "0";
355 # Allow to use different bundler version than the lockfile has
356 env.BUNDLER_VERSION = pkgs.bundler.version;
357
358 # requires full git and repository, even a src `leaveDotGit` is not enough. So patch this function to return the version
359 postPatch = ''
360 substituteInPlace script/assemble_ember_build.rb --replace-fail "def core_tree_hash" "def core_tree_hash; return \"v${version}\""
361 '';
362
363 # We have to set up an environment that is close enough to
364 # production ready or the assets:precompile task refuses to
365 # run. This means that Redis and PostgreSQL has to be running and
366 # database migrations performed.
367 preBuild = ''
368 # Patch before running postinstall hook script
369 patchShebangs node_modules/
370 patchShebangs --build frontend/
371 export SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt
372
373 redis-server >/dev/null &
374
375 initdb -A trust $NIX_BUILD_TOP/postgres >/dev/null
376 postgres -D $NIX_BUILD_TOP/postgres -k $NIX_BUILD_TOP >/dev/null &
377 export PGHOST=$NIX_BUILD_TOP
378
379 echo "Waiting for Redis and PostgreSQL to be ready.."
380 while ! redis-cli --scan >/dev/null || ! psql -l >/dev/null; do
381 sleep 0.1
382 done
383
384 psql -d postgres -tAc 'CREATE USER "discourse"'
385 psql -d postgres -tAc 'CREATE DATABASE "discourse" OWNER "discourse"'
386 psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
387 psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS hstore"
388 psql 'discourse' -tAc "CREATE EXTENSION IF NOT EXISTS vector"
389
390 ${lib.concatMapStringsSep "\n" (p: "ln -sf ${p} plugins/${p.pluginName or ""}") plugins}
391
392 bundle exec rake db:migrate >/dev/null
393 chmod -R +w tmp
394 '';
395
396 buildPhase = ''
397 runHook preBuild
398
399 patchShebangs script/
400 bundle exec rake assets:precompile
401
402 runHook postBuild
403 '';
404
405 installPhase = ''
406 runHook preInstall
407
408 mv public/assets $out
409
410 mv node_modules $node_modules
411
412 mv app/assets/generated $generated
413 mv frontend $frontend
414
415 runHook postInstall
416 '';
417
418 # The node_modules output by design has broken symlinks, as it refers to the source code.
419 # They are resolved in the primary discourse derivation.
420 dontCheckForBrokenSymlinks = true;
421 };
422
423 discourse = stdenv.mkDerivation {
424 pname = "discourse";
425 inherit version src;
426
427 buildInputs = [
428 rubyEnv
429 rubyEnv.wrappedRuby
430 rubyEnv.bundler
431 ];
432
433 patches = [
434 # Load a separate NixOS site settings file
435 ./nixos_defaults.patch
436
437 # Add a noninteractive admin creation task
438 ./admin_create.patch
439
440 # Add the path to the CA cert bundle to make TLS work
441 ./action_mailer_ca_cert.patch
442
443 # Log Unicorn messages to the journal and make request timeout
444 # configurable
445 ./unicorn_logging_and_timeout.patch
446
447 # Use the Ruby API version in the plugin gem path, to match the
448 # one constructed by bundlerEnv
449 ./plugin_gem_api_version.patch
450
451 # Change the path to the auto generated plugin assets, which
452 # defaults to the plugin's directory and isn't writable at the
453 # time of asset generation
454 ./auto_generated_path.patch
455
456 # Make sure the notification email setting applies
457 ./notification_email.patch
458
459 # Little does he know, so he decided there is no need to generate the
460 # theme-transpiler over and over again. Which at the same time allows the removal
461 # of javascript devDependencies from the runtime environment.
462 ./prebuild-asset-processor.patch
463
464 # Our app/assets/generated folder is a symlink, but the ruby File.mkdir_p doesn't allow
465 # a symlink in the way to the last directory. This patch explicitly resolves the symlink.
466 ./resolve_generated_assets_symlink.patch
467 ];
468
469 postPatch = ''
470 # Always require lib-files and application.rb through their store
471 # path, not their relative state directory path. This gets rid of
472 # warnings and means we don't have to link back to lib from the
473 # state directory.
474 find config -type f -name "*.rb" -execdir \
475 sed -Ei "s,(\.\./)+(lib|app)/,$out/share/discourse/\2/," {} \;
476 find config -maxdepth 1 -type f -name "*.rb" -execdir \
477 sed -Ei "s,require_relative (\"|')([[:alnum:]].*)(\"|'),require_relative '$out/share/discourse/config/\2'," {} \;
478 '';
479
480 buildPhase = ''
481 runHook preBuild
482
483 mv config config.dist
484 mv public public.dist
485
486 runHook postBuild
487 '';
488
489 installPhase = ''
490 runHook preInstall
491
492 mkdir -p $out/share
493 cp -r . $out/share/discourse
494 rm -r $out/share/discourse/log
495 ln -sf /var/log/discourse $out/share/discourse/log
496 ln -sf /var/lib/discourse/tmp $out/share/discourse/tmp
497 ln -sf /run/discourse/config $out/share/discourse/config
498 ln -sf /run/discourse/public $out/share/discourse/public
499 ln -sf /run/discourse/assets-generated $out/share/discourse/app/assets/generated
500 ln -sf ${assets.node_modules} $out/share/discourse/node_modules
501 ln -sf ${assets} $out/share/discourse/public.dist/assets
502 # This needs to be copied because it contains symlinks to node_modules
503 rm -r $out/share/discourse/frontend
504 cp -r ${assets.frontend} $out/share/discourse/frontend
505 ${lib.concatMapStringsSep "\n" (
506 p: "ln -sf ${p} $out/share/discourse/plugins/${p.pluginName or ""}"
507 ) plugins}
508
509 runHook postInstall
510 '';
511
512 passthru = {
513 inherit
514 rubyEnv
515 runtimeEnv
516 runtimeDeps
517 rake
518 mkDiscoursePlugin
519 assets
520 ;
521 inherit (pkgs)
522 discourseAllPlugins
523 ;
524 enabledPlugins = plugins;
525 plugins = callPackage ./plugins/all-plugins.nix { inherit mkDiscoursePlugin; };
526 ruby = rubyEnv.wrappedRuby;
527 tests = {
528 inherit (nixosTests)
529 discourse
530 discourseAllPlugins
531 ;
532 };
533 };
534 meta = {
535 homepage = "https://www.discourse.org/";
536 platforms = lib.platforms.linux;
537 maintainers = with lib.maintainers; [ talyz ];
538 license = lib.licenses.gpl2Plus;
539 description = "Open source discussion platform";
540 };
541 };
542in
543discourse