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