3
0
Fork 0
forked from mirrors/nixpkgs

Merge pull request #129464 from talyz/discourse-plugins

discourse: Updates and fixes
This commit is contained in:
Kim Lindberger 2021-07-15 14:50:28 +02:00 committed by GitHub
commit 4dcf295417
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 250 additions and 85 deletions

View file

@ -475,21 +475,16 @@ in
plugins = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [];
example = ''
[
(pkgs.fetchFromGitHub {
owner = "discourse";
repo = "discourse-spoiler-alert";
rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
})
example = lib.literalExample ''
with config.services.discourse.package.plugins; [
discourse-canned-replies
discourse-github
];
'';
description = ''
<productname>Discourse</productname> plugins to install as a
list of derivations. As long as a plugin supports the
standard install method, packaging it should only require
fetching its source with an appropriate fetcher.
Plugins to install as part of
<productname>Discourse</productname>, expressed as a list of
derivations.
'';
};

View file

@ -262,9 +262,31 @@ services.discourse = {
<para>
You can install <productname>Discourse</productname> plugins
using the <xref linkend="opt-services.discourse.plugins" />
option. As long as a plugin supports the standard install
method, packaging it should only require fetching its source
with an appropriate fetcher.
option. Pre-packaged plugins are provided in
<literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
you want the full suite of plugins provided through
<literal>nixpkgs</literal>, you can also set the <xref
linkend="opt-services.discourse.package" /> option to
<literal>pkgs.discourseAllPlugins</literal>.
</para>
<para>
Plugins can be built with the
<literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
function. Normally, it should suffice to provide a
<literal>name</literal> and <literal>src</literal> attribute. If
the plugin has Ruby dependencies, however, they need to be
packaged in accordance with the <link
xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
with Ruby</link> section of the Nixpkgs manual and the
appropriate gem options set in <literal>bundlerEnvArgs</literal>
(normally <literal>gemdir</literal> is sufficient). A plugin's
Ruby dependencies are listed in its
<filename>plugin.rb</filename> file as function calls to
<literal>gem</literal>. To construct the corresponding
<filename>Gemfile</filename>, run <command>bundle
init</command>, then add the <literal>gem</literal> lines to it
verbatim.
</para>
<para>
@ -280,7 +302,10 @@ services.discourse = {
<para>
For example, to add the <link
xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
plugin and disable it by default:
and <link
xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
plugins, and disable <literal>discourse-spoiler-alert</literal>
by default:
<programlisting>
services.discourse = {
@ -301,13 +326,9 @@ services.discourse = {
<link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
};
<link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
<link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = [
(pkgs.fetchFromGitHub {
owner = "discourse";
repo = "discourse-spoiler-alert";
rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
})
<link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
discourse-spoiler-alert
discourse-solved
];
<link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
plugins = {

View file

@ -9,13 +9,13 @@
}:
let
version = "2.7.4";
version = "2.7.5";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse";
rev = "v${version}";
sha256 = "sha256-3cvrdWBXRM5F8qFEqbe8ru1U0wBqCkRxK7GAV0beJNk=";
sha256 = "sha256-OykWaiBAHcZy41i+aRzBHCRgwnfQUBijHjb+ofIk25M=";
};
runtimeDeps = [
@ -64,7 +64,6 @@ let
});
in
stdenv.mkDerivation (builtins.removeAttrs args [ "bundlerEnvArgs" ] // {
inherit name pname version src meta;
pluginName = if name != null then name else "${pname}-${version}";
phases = [ "unpackPhase" "installPhase" ];
installPhase = ''
@ -151,6 +150,7 @@ let
brotli
procps
nodePackages.uglify-js
nodePackages.terser
];
patches = [

View file

@ -3,10 +3,10 @@ let
callPackage = newScope args;
in
{
discourse-spoiler-alert = callPackage ./discourse-spoiler-alert {};
discourse-solved = callPackage ./discourse-solved {};
discourse-canned-replies = callPackage ./discourse-canned-replies {};
discourse-math = callPackage ./discourse-math {};
discourse-github = callPackage ./discourse-github {};
discourse-math = callPackage ./discourse-math {};
discourse-solved = callPackage ./discourse-solved {};
discourse-spoiler-alert = callPackage ./discourse-spoiler-alert {};
discourse-yearly-review = callPackage ./discourse-yearly-review {};
}

View file

@ -1,11 +1,17 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-canned-replies";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-canned-replies";
rev = "7ee748f18a276aca42185e2079c1d4cadeecdaf8";
sha256 = "0j10kxfr6v2rdd58smg2i7iac46z74qnnjk8b91jd1svazhis1ph";
rev = "e3f1de8928df5955b64994079b7e2073556e5456";
sha256 = "1g4fazm6cn6hbfd08mq2zhc6dgm4qj1r1f1amhbgxhk6qsxf42cd";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-canned-replies";
maintainers = with maintainers; [ talyz ];
license = licenses.gpl2Only;
description = "Adds support for inserting a canned reply into the composer window via a UI";
};
}

View file

@ -1,3 +1,9 @@
source 'https://rubygems.org'
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
# gem "rails"
gem 'sawyer', '0.8.2'
gem 'octokit', '4.21.0'

View file

@ -1,21 +1,25 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
faraday (1.4.2)
faraday (1.5.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
faraday-patron (1.0.0)
multipart-post (2.1.1)
octokit (4.21.0)
faraday (>= 0.9)
@ -27,11 +31,11 @@ GEM
faraday (> 0.8, < 2.0)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
octokit (= 4.21.0)
sawyer (= 0.8.2)
BUNDLED WITH
2.1.4
2.2.20

View file

@ -1,4 +1,4 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-github";
@ -6,7 +6,14 @@ mkDiscoursePlugin {
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-github";
rev = "151e353a5a1971157c70c2e2b0f56387f212a81f";
sha256 = "00kra6zd2k1f2vwcdvxnxnammzh72f5qxcqbb94m0z6maj598wdy";
rev = "154fd5ea597640c2259ce489b4ce75b48ac1973c";
sha256 = "0wb5p219z42rc035rnh2iwrbsj000nxa9shbmc325rzcg6xlhdhw";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-github";
maintainers = with maintainers; [ talyz ];
license = licenses.mit;
description = "Adds GitHub badges and linkback functionality";
};
}

View file

@ -5,21 +5,21 @@
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "1fvchp2rhp2rmigx7qglf69xvjqvzq7x0g49naliw29r2bz656sy";
sha256 = "022r3m9wdxljpbya69y2i3h9g3dhhfaqzidf95m6qjzms792jvgp";
type = "gem";
};
version = "2.7.0";
version = "2.8.0";
};
faraday = {
dependencies = ["faraday-em_http" "faraday-em_synchrony" "faraday-excon" "faraday-net_http" "faraday-net_http_persistent" "multipart-post" "ruby2_keywords"];
dependencies = ["faraday-em_http" "faraday-em_synchrony" "faraday-excon" "faraday-httpclient" "faraday-net_http" "faraday-net_http_persistent" "faraday-patron" "multipart-post" "ruby2_keywords"];
groups = ["default"];
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "07mhk70gv453pg38md346470hknyhipdqppnplq706ll3k3lzb7v";
sha256 = "0gwbii45plm9bljk22bwzhzxrc5xid8qx24f54vrm74q3zaz00ah";
type = "gem";
};
version = "1.4.2";
version = "1.5.0";
};
faraday-em_http = {
groups = ["default"];
@ -51,6 +51,16 @@
};
version = "1.1.0";
};
faraday-httpclient = {
groups = ["default"];
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "0fyk0jd3ks7fdn8nv3spnwjpzx2lmxmg2gh4inz3by1zjzqg33sc";
type = "gem";
};
version = "1.0.1";
};
faraday-net_http = {
groups = ["default"];
platforms = [];
@ -71,6 +81,16 @@
};
version = "1.1.0";
};
faraday-patron = {
groups = ["default"];
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "19wgsgfq0xkski1g7m96snv39la3zxz6x7nbdgiwhg5v82rxfb6w";
type = "gem";
};
version = "1.0.0";
};
multipart-post = {
groups = ["default"];
platforms = [];

View file

@ -1,11 +1,17 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-math";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-math";
rev = "143ddea4558ea9a1b3fd71635bc11e055763c8e7";
sha256 = "18pq5ybl3g34i39cpixc3nszvq8gx5yji58zlbbl6428mm011cbx";
rev = "aed0c83cee568d5239143bcf1df59c5fbe86b276";
sha256 = "1k6kpnhf8s2l0w9zr5pn3wvn8w0n3gwkv7qkv0mkhkzy246ag20z";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-math";
maintainers = with maintainers; [ talyz ];
license = licenses.mit;
description = "Official MathJax support for Discourse";
};
}

View file

@ -1,11 +1,17 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-solved";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-solved";
rev = "179611766d53974308e6f7def21836997c3c55fc";
sha256 = "sha256:1s77h42d3bv2lqw33akxh8ss482vxnz4d7qz6xicwqfwv34qjf03";
rev = "b96374bf4ab7e6d5cecb0761918b060a524eb9bf";
sha256 = "0zrv70p0wz93akpcj6gpwjkw7az3iz9bx4n2z630kyrlmxdbj32a";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-solved";
maintainers = with maintainers; [ talyz ];
license = licenses.mit;
description = "Allow accepted answers on topics";
};
}

View file

@ -1,11 +1,17 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-spoiler-alert";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-spoiler-alert";
rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
rev = "ec14a2316da0a4fc055cfc21c68a60040188a2b4";
sha256 = "11n977gp8va7jkqa6i3ja279k4nmkhk5l4hg9xhs229450m1rnfp";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-spoiler-alert";
maintainers = with maintainers; [ talyz ];
license = licenses.mit;
description = "Hide spoilers behind the spoiler-alert jQuery plugin";
};
}

View file

@ -1,11 +1,17 @@
{ mkDiscoursePlugin, fetchFromGitHub }:
{ lib, mkDiscoursePlugin, fetchFromGitHub }:
mkDiscoursePlugin {
name = "discourse-yearly-review";
src = fetchFromGitHub {
owner = "discourse";
repo = "discourse-yearly-review";
rev = "d1471bdb68945f55342e72e2c525b4f628419a50";
sha256 = "sha256:0xpl0l1vpih8xzb6y7k1lm72nj4ya99378viyhqfvpwzsn5pha2a";
rev = "95149df2282d62eebeb265b4895df15a2b259d03";
sha256 = "02n27al8n8cxz3dx4awlnd4qhv8a0fmjac57yyblmpviapja1wj7";
};
meta = with lib; {
homepage = "https://github.com/discourse/discourse-yearly-review";
maintainers = with maintainers; [ talyz ];
license = licenses.mit;
description = "Publishes an automated Year in Review topic";
};
}

View file

@ -186,11 +186,6 @@ GEM
jwt (2.2.3)
kgio (2.11.3)
libv8-node (15.14.0.1)
libv8-node (15.14.0.1-arm64-darwin-20)
libv8-node (15.14.0.1-x86_64-darwin-18)
libv8-node (15.14.0.1-x86_64-darwin-19)
libv8-node (15.14.0.1-x86_64-darwin-20)
libv8-node (15.14.0.1-x86_64-linux)
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@ -214,7 +209,7 @@ GEM
rack (>= 1.1.3)
method_source (1.0.0)
mini_mime (1.1.0)
mini_portile2 (2.5.1)
mini_portile2 (2.5.3)
mini_racer (0.4.0)
libv8-node (~> 15.14.0.0)
mini_scheduler (0.13.0)
@ -232,7 +227,7 @@ GEM
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.7)
nokogiri (1.11.5)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogumbo (2.0.5)
@ -267,7 +262,7 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (2.2.15)
onebox (2.2.17)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
@ -465,12 +460,7 @@ GEM
zeitwerk (2.4.2)
PLATFORMS
arm64-darwin-20
ruby
x86_64-darwin-18
x86_64-darwin-19
x86_64-darwin-20
x86_64-linux
DEPENDENCIES
actionmailer (= 6.1.3.2)
@ -600,4 +590,4 @@ DEPENDENCIES
yaml-lint
BUNDLED WITH
2.2.16
2.2.20

View file

@ -1135,10 +1135,10 @@
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "0xg1x4708a4pn2wk8qs2d8kfzzdyv9kjjachg2f1phsx62ap2rx2";
sha256 = "1ad0mli9rc0f17zw4ibp24dbj1y39zkykijsjmnzl4gwpg5s0j6k";
type = "gem";
};
version = "2.5.1";
version = "2.5.3";
};
mini_racer = {
dependencies = ["libv8-node"];
@ -1284,10 +1284,10 @@
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "1i80ny61maqzqr1fq5wgpkijmh5j8abisrmhn16kv7mzmxqg5w0m";
sha256 = "1vrn31385ix5k9b0yalnlzv360isv6dincbcvi8psllnwz4sjxj9";
type = "gem";
};
version = "1.11.5";
version = "1.11.7";
};
nokogumbo = {
dependencies = ["nokogiri"];
@ -1414,10 +1414,10 @@
platforms = [];
source = {
remotes = ["https://rubygems.org"];
sha256 = "0a76xmwikcg2lv8k2cawzhmi2hx7j145v12mbpriby6zff797z4g";
sha256 = "1swlysqwfc6mb7smv52yv12sd79dchjf2f6r738wrag0wp5hazqy";
type = "gem";
};
version = "2.2.15";
version = "2.2.17";
};
openssl = {
groups = ["default"];

View file

@ -1,5 +1,5 @@
#!/usr/bin/env nix-shell
#! nix-shell -i python3 -p bundix bundler nix-update python3 python3Packages.requests python3Packages.click python3Packages.click-log
#! nix-shell -i python3 -p bundix bundler nix-update nix-universal-prefetch python3 python3Packages.requests python3Packages.click python3Packages.click-log
import click
import click_log
@ -8,17 +8,22 @@ import tempfile
import re
import logging
import subprocess
import pathlib
import os
import stat
import json
import requests
from distutils.version import LooseVersion
from pathlib import Path
from typing import Iterable
import requests
logger = logging.getLogger(__name__)
class DiscourseRepo:
version_regex = re.compile(r'^v\d+\.\d+\.\d+$')
_latest_commit_sha = None
def __init__(self, owner: str = 'discourse', repo: str = 'discourse'):
self.owner = owner
self.repo = repo
@ -35,6 +40,15 @@ class DiscourseRepo:
versions.sort(key=lambda x: LooseVersion(x.replace('v', '')), reverse=True)
return versions
@property
def latest_commit_sha(self) -> str:
if self._latest_commit_sha is None:
r = requests.get(f'https://api.github.com/repos/{self.owner}/{self.repo}/commits?per_page=1')
r.raise_for_status()
self._latest_commit_sha = r.json()[0]['sha']
return self._latest_commit_sha
@staticmethod
def rev2version(tag: str) -> str:
"""
@ -57,19 +71,23 @@ class DiscourseRepo:
def _call_nix_update(pkg, version):
"""calls nix-update from nixpkgs root dir"""
nixpkgs_path = pathlib.Path(__file__).parent / '../../../../'
nixpkgs_path = Path(__file__).parent / '../../../../'
return subprocess.check_output(['nix-update', pkg, '--version', version], cwd=nixpkgs_path)
def _nix_eval(expr: str):
nixpkgs_path = Path(__file__).parent / '../../../../'
return json.loads(subprocess.check_output(['nix', 'eval', '--json', f'(with import {nixpkgs_path} {{}}; {expr})'], text=True))
def _get_current_package_version(pkg: str):
nixpkgs_path = pathlib.Path(__file__).parent / '../../../../'
return subprocess.check_output(['nix', 'eval', '--raw', f'nixpkgs.{pkg}.version'], text=True)
return _nix_eval(f'{pkg}.version')
def _diff_file(filepath: str, old_version: str, new_version: str):
repo = DiscourseRepo()
current_dir = pathlib.Path(__file__).parent
current_dir = Path(__file__).parent
old = repo.get_file(filepath, 'v' + old_version)
new = repo.get_file(filepath, 'v' + new_version)
@ -148,17 +166,87 @@ def update(rev):
version = repo.rev2version(rev)
logger.debug(f"Using version {version}")
rubyenv_dir = pathlib.Path(__file__).parent / "rubyEnv"
rubyenv_dir = Path(__file__).parent / "rubyEnv"
for fn in ['Gemfile.lock', 'Gemfile']:
with open(rubyenv_dir / fn, 'w') as f:
f.write(repo.get_file(fn, rev))
subprocess.check_output(['bundle', 'lock'], cwd=rubyenv_dir)
for platform in ['arm64-darwin-20', 'x86_64-darwin-18',
'x86_64-darwin-19', 'x86_64-darwin-20',
'x86_64-linux']:
subprocess.check_output(['bundle', 'lock', '--remove-platform', platform], cwd=rubyenv_dir)
subprocess.check_output(['bundix'], cwd=rubyenv_dir)
_call_nix_update('discourse', repo.rev2version(rev))
@cli.command()
def update_plugins():
"""Update plugins to their latest revision.
"""
plugins = [
{'name': 'discourse-canned-replies'},
{'name': 'discourse-github'},
{'name': 'discourse-math'},
{'name': 'discourse-solved'},
{'name': 'discourse-spoiler-alert'},
{'name': 'discourse-yearly-review'},
]
for plugin in plugins:
fetcher = plugin.get('fetcher') or "fetchFromGitHub"
owner = plugin.get('owner') or "discourse"
name = plugin.get('name')
repo_name = plugin.get('repo_name') or name
repo = DiscourseRepo(owner=owner, repo=repo_name)
prev_commit_sha = _nix_eval(f'discourse.plugins.{name}.src.rev')
if prev_commit_sha == repo.latest_commit_sha:
click.echo(f'Plugin {name} is already at the latest revision')
continue
filename = _nix_eval(f'builtins.unsafeGetAttrPos "src" discourse.plugins.{name}')['file']
prev_hash = _nix_eval(f'discourse.plugins.{name}.src.outputHash')
new_hash = subprocess.check_output([
'nix-universal-prefetch', fetcher,
'--owner', owner,
'--repo', repo_name,
'--rev', repo.latest_commit_sha,
], text=True).strip("\n")
click.echo(f"Update {name}, {prev_commit_sha} -> {repo.latest_commit_sha} in {filename}")
with open(filename, 'r+') as f:
content = f.read()
content = content.replace(prev_commit_sha, repo.latest_commit_sha)
content = content.replace(prev_hash, new_hash)
f.seek(0)
f.write(content)
f.truncate()
rubyenv_dir = Path(filename).parent
gemfile = rubyenv_dir / "Gemfile"
gemfile_text = ''
for line in repo.get_file('plugin.rb', repo.latest_commit_sha).splitlines():
if 'gem ' in line:
gemfile_text = gemfile_text + line + os.linesep
if len(gemfile_text) > 0:
if os.path.isfile(gemfile):
os.remove(gemfile)
subprocess.check_output(['bundle', 'init'], cwd=rubyenv_dir)
os.chmod(gemfile, stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH)
with open(gemfile, 'a') as f:
f.write(gemfile_text)
subprocess.check_output(['bundle', 'lock', '--update'], cwd=rubyenv_dir)
subprocess.check_output(['bundix'], cwd=rubyenv_dir)
if __name__ == '__main__':
cli()

View file

@ -2487,6 +2487,10 @@ in
discourse = callPackage ../servers/web-apps/discourse { };
discourseAllPlugins = discourse.override {
plugins = lib.filter (p: p ? pluginName) (builtins.attrValues discourse.plugins);
};
discourse-mail-receiver = callPackage ../servers/web-apps/discourse/mail_receiver { };
discocss = callPackage ../tools/misc/discocss { };