reaction: 2.2.1 -> 2.3.0; reaction-plugins: init (#490062)
This commit is contained in:
@@ -7,10 +7,21 @@
|
||||
let
|
||||
settingsFormat = pkgs.formats.yaml { };
|
||||
|
||||
cfg = config.services.reaction;
|
||||
|
||||
inherit (lib)
|
||||
mkOption
|
||||
concatMapStringsSep
|
||||
filterAttrs
|
||||
getExe
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
mapAttrs
|
||||
optional
|
||||
optionals
|
||||
optionalString
|
||||
types
|
||||
;
|
||||
in
|
||||
@@ -30,7 +41,65 @@ in
|
||||
default = { };
|
||||
type = types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
options = { };
|
||||
options = {
|
||||
plugins = mkOption {
|
||||
description = ''
|
||||
Nixpkgs provides a `reaction-plugins` package set which includes both offical and community plugins for reaction.
|
||||
|
||||
To use the plugins in your module configuration, in `settings.plugins` you can use for e.g. `''${lib.getExe reaction-plugins.reaction-plugin-ipset}`
|
||||
See https://reaction.ppom.me/plugins/ to configure plugins.
|
||||
'';
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
enable = mkOption {
|
||||
description = "enable reaction-plugin-${name}";
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
path = mkOption {
|
||||
description = "path to the plugin binary";
|
||||
type = types.str;
|
||||
default = "${cfg.package.plugins."reaction-plugin-${name}"}/bin/reaction-plugin-${name}";
|
||||
defaultText = lib.literalExpression ''''${cfg.package.plugins."reaction-plugin-${name}"}/bin/reaction-plugin-${name}'';
|
||||
};
|
||||
check_root = mkOption {
|
||||
description = "Whether reaction must check that the executable is owned by root";
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
systemd = mkOption {
|
||||
description = "Whether reaction must isolate the plugin using systemd's run0";
|
||||
type = types.bool;
|
||||
default = cfg.runAsRoot;
|
||||
defaultText = "config.services.reaction.runAsRoot";
|
||||
};
|
||||
systemd_options = mkOption {
|
||||
description = ''
|
||||
A key-value map of systemd options.
|
||||
Keys must be strings and values must be string arrays.
|
||||
|
||||
See `man systemd.directives` for all supported options, and particularly options in `man systemd.exec`
|
||||
'';
|
||||
type = types.attrsOf (types.listOf types.str);
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
# Filter plugins which are disabled
|
||||
apply =
|
||||
self:
|
||||
lib.pipe self [
|
||||
(filterAttrs (name: p: p.enable))
|
||||
(mapAttrs (name: p: removeAttrs p [ "enable" ]))
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,25 +182,39 @@ in
|
||||
services.openssh.settings.LogLevel = lib.mkDefault "VERBOSE";
|
||||
}
|
||||
```
|
||||
|
||||
```nix
|
||||
# core ipset plugin requires these if running as non-root
|
||||
systemd.services.reaction.serviceConfig = {
|
||||
CapabilityBoundingSet = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_DAC_READ_SEARCH" # for journalctl
|
||||
];
|
||||
AmbientCapabilities = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_DAC_READ_SEARCH"
|
||||
];
|
||||
};
|
||||
```
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
cfg = config.services.reaction;
|
||||
|
||||
generatedSettings = settingsFormat.generate "reaction.yml" cfg.settings;
|
||||
settingsDir = pkgs.runCommand "reaction-settings-dir" { } ''
|
||||
mkdir -p $out
|
||||
${lib.concatMapStringsSep "\n" (file: ''
|
||||
${concatMapStringsSep "\n" (file: ''
|
||||
filename=$(basename "${file}")
|
||||
ln -s "${file}" "$out/$filename"
|
||||
'') cfg.settingsFiles}
|
||||
ln -s ${generatedSettings} $out/reaction.yml
|
||||
'';
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.settings != { } || (builtins.length cfg.settingsFiles) != 0;
|
||||
@@ -139,7 +222,7 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
users = lib.mkIf (!cfg.runAsRoot) {
|
||||
users = mkIf (!cfg.runAsRoot) {
|
||||
users.reaction = {
|
||||
isSystemUser = true;
|
||||
group = "reaction";
|
||||
@@ -148,27 +231,29 @@ in
|
||||
};
|
||||
|
||||
system.checks =
|
||||
lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform)
|
||||
optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform)
|
||||
(
|
||||
pkgs.runCommand "reaction-config-validation" { } ''
|
||||
${lib.getExe cfg.package} test-config -c ${settingsDir} >/dev/null
|
||||
${getExe cfg.package} test-config -c ${settingsDir} >/dev/null
|
||||
echo "reaction config ${settingsDir} is valid"
|
||||
touch $out
|
||||
''
|
||||
);
|
||||
|
||||
systemd.services.reaction = {
|
||||
description = "Scan logs and take action";
|
||||
description = "A daemon that scans program outputs for repeated patterns, and takes action.";
|
||||
documentation = [ "https://reaction.ppom.me" ];
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
partOf = lib.optionals cfg.stopForFirewall [ "firewall.service" ];
|
||||
partOf = optionals cfg.stopForFirewall [ "firewall.service" ];
|
||||
path = [ pkgs.iptables ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
KillMode = "mixed"; # for plugins
|
||||
User = if (!cfg.runAsRoot) then "reaction" else "root";
|
||||
ExecStart = ''
|
||||
${lib.getExe cfg.package} start -c ${settingsDir}${
|
||||
lib.optionalString (cfg.loglevel != null) " -l ${cfg.loglevel}"
|
||||
${getExe cfg.package} start -c ${settingsDir}${
|
||||
optionalString (cfg.loglevel != null) " -l ${cfg.loglevel}"
|
||||
}
|
||||
'';
|
||||
|
||||
@@ -197,6 +282,20 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# pre-configure official plugins
|
||||
services.reaction.settings.plugins = {
|
||||
ipset = {
|
||||
enable = mkDefault true;
|
||||
systemd_options = {
|
||||
CapabilityBoundingSet = [
|
||||
"~CAP_NET_ADMIN"
|
||||
"~CAP_PERFMON"
|
||||
];
|
||||
};
|
||||
};
|
||||
virtual.enable = mkDefault true;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
};
|
||||
|
||||
|
||||
@@ -1386,8 +1386,10 @@ in
|
||||
rasdaemon = runTest ./rasdaemon.nix;
|
||||
rathole = runTest ./rathole.nix;
|
||||
rauc = runTest ./rauc.nix;
|
||||
reaction = runTest ./reaction.nix;
|
||||
reaction-firewall = runTest ./reaction-firewall.nix;
|
||||
reaction = import ./reaction {
|
||||
inherit (pkgs) lib;
|
||||
inherit runTest;
|
||||
};
|
||||
readarr = runTest ./readarr.nix;
|
||||
readeck = runTest ./readeck.nix;
|
||||
realm = runTest ./realm.nix;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
services.reaction = {
|
||||
enable = true;
|
||||
stopForFirewall = false;
|
||||
# example.jsonnet/example.yml can be copied and modified from ${pkgs.reaction}/share/examples
|
||||
# example.jsonnet or example.yml can be copied and modified from ${pkgs.reaction}/share/examples
|
||||
settingsFiles = [ "${pkgs.reaction}/share/examples/example.jsonnet" ];
|
||||
runAsRoot = false;
|
||||
};
|
||||
@@ -92,6 +92,7 @@
|
||||
{
|
||||
# not needed, only for manual interactive debugging
|
||||
virtualisation.memorySize = 4096;
|
||||
virtualisation.graphics = false;
|
||||
environment.systemPackages = with pkgs; [
|
||||
btop
|
||||
sysz
|
||||
@@ -0,0 +1,6 @@
|
||||
{ lib, runTest }:
|
||||
lib.recurseIntoAttrs {
|
||||
basic = runTest ./basic.nix;
|
||||
firewall = runTest ./firewall.nix;
|
||||
plugins = runTest ./plugins.nix;
|
||||
}
|
||||
@@ -17,6 +17,12 @@
|
||||
# "${pkgs.reaction}/share/examples/example.yml" # can't specify both because conflicts
|
||||
];
|
||||
runAsRoot = true;
|
||||
settings = {
|
||||
# In the qemu vm `run0 ls` as root prints nothing, so we can't use it
|
||||
# see https://reaction.ppom.me/reference.html#systemd
|
||||
plugins.ipset.systemd = false;
|
||||
plugins.virtual.systemd = false;
|
||||
};
|
||||
};
|
||||
networking.firewall.enable = true;
|
||||
};
|
||||
@@ -68,6 +74,9 @@
|
||||
# - nix run .#nixosTests.reaction-firewall.driverInteractive -L
|
||||
# - run_tests()
|
||||
interactive.sshBackdoor.enable = true; # ssh -o User=root vsock%3
|
||||
interactive.nodes.machine = _: {
|
||||
virtualisation.graphics = false;
|
||||
};
|
||||
|
||||
meta.maintainers =
|
||||
with lib.maintainers;
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "reaction-core-plugins";
|
||||
|
||||
nodes.server = args: {
|
||||
services.reaction = {
|
||||
enable = true;
|
||||
stopForFirewall = false;
|
||||
runAsRoot = false;
|
||||
settings = import ./settings.nix args;
|
||||
/*
|
||||
# NOTE: When runAsRoot is true, disable run0
|
||||
settings = {
|
||||
# In the qemu vm `run0 ls` as root prints nothing, so we can't use it
|
||||
# see https://reaction.ppom.me/reference.html#systemd
|
||||
plugins.ipset.systemd = false;
|
||||
plugins.virtual.systemd = false;
|
||||
};
|
||||
*/
|
||||
};
|
||||
/*
|
||||
NOTE:
|
||||
- if reaction is run as non-root, the plugins need these capabilities, remove these if runAsRoot is true
|
||||
- CAP_DAC_READ_SEARCH is for journalctl for accessing ssh logs
|
||||
- useful tools: capable (from package bcc), captree, getpcaps (from libpcap)
|
||||
*/
|
||||
systemd.services.reaction.serviceConfig = {
|
||||
CapabilityBoundingSet = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_DAC_READ_SEARCH"
|
||||
];
|
||||
AmbientCapabilities = [
|
||||
"CAP_NET_ADMIN"
|
||||
"CAP_NET_RAW"
|
||||
"CAP_DAC_READ_SEARCH"
|
||||
];
|
||||
};
|
||||
services.openssh.enable = true;
|
||||
|
||||
users.users.nixos.isNormalUser = true; # neeeded to establish a ssh connection, by default root login is succeeding without any password
|
||||
};
|
||||
|
||||
nodes.client = _: {
|
||||
environment.systemPackages = [
|
||||
pkgs.sshpass
|
||||
pkgs.libressl.nc
|
||||
];
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }: # py
|
||||
''
|
||||
start_all()
|
||||
|
||||
# Wait for everything to be ready.
|
||||
server.wait_for_unit("multi-user.target")
|
||||
server.wait_for_unit("reaction")
|
||||
server.wait_for_unit("sshd")
|
||||
|
||||
client_addr = "${(lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address}"
|
||||
server_addr = "${(lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address}"
|
||||
|
||||
# Verify there is not ban and the port is reachable from the client.
|
||||
server.succeed(f"reaction show | grep -q {client_addr} || test $? -eq 1")
|
||||
client.succeed(f"nc -w3 -z {server_addr} 22")
|
||||
|
||||
# Cause authentication failure log entries.
|
||||
for _ in range(2):
|
||||
client.fail(f"""
|
||||
sshpass -p 'wrongpassword' \
|
||||
ssh -o StrictHostKeyChecking=no \
|
||||
-o User=nixos \
|
||||
-o ServerAliveInterval=1 \
|
||||
-o ServerAliveCountMax=2 \
|
||||
{server_addr}
|
||||
""")
|
||||
|
||||
# Verify there is a ban and the port is unreachable from the client.
|
||||
server.sleep(2)
|
||||
output = server.succeed("reaction show")
|
||||
print(output)
|
||||
assert client_addr in output, f"client did not get banned, {client_addr}"
|
||||
|
||||
client.fail(f"nc -w3 -z {server_addr} 22")
|
||||
|
||||
# Check that unbanning works
|
||||
output = server.succeed("reaction flush")
|
||||
print(output)
|
||||
|
||||
client.succeed(f"nc -w3 -z {server_addr} 22")
|
||||
'';
|
||||
|
||||
# Debug interactively with:
|
||||
# - nix run .#nixosTests.reaction.driverInteractive -L
|
||||
# - run_tests()
|
||||
# ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.)
|
||||
# ssh -o User=root vsock%4
|
||||
interactive.sshBackdoor.enable = true;
|
||||
|
||||
interactive.nodes.server =
|
||||
{ config, ... }:
|
||||
{
|
||||
# not needed, only for manual interactive debugging
|
||||
virtualisation.memorySize = 4096;
|
||||
virtualisation.graphics = false;
|
||||
environment.systemPackages = with pkgs; [
|
||||
btop
|
||||
sysz
|
||||
sshpass
|
||||
libressl.nc
|
||||
];
|
||||
};
|
||||
|
||||
meta.maintainers =
|
||||
with lib.maintainers;
|
||||
[
|
||||
ppom
|
||||
]
|
||||
++ lib.teams.ngi.members;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{ config, ... }:
|
||||
let
|
||||
banFor = name: duration: {
|
||||
ban = {
|
||||
type = "ipset";
|
||||
options = {
|
||||
set = "reaction-${name}";
|
||||
action = "add";
|
||||
};
|
||||
};
|
||||
unban = {
|
||||
after = duration;
|
||||
type = "ipset";
|
||||
options = {
|
||||
set = "reaction-${name}";
|
||||
action = "del";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
journalctl = "${config.systemd.package}/bin/journalctl";
|
||||
in
|
||||
{
|
||||
patterns = {
|
||||
ip = {
|
||||
type = "ip";
|
||||
ipv6mask = 64;
|
||||
ignore = [
|
||||
"127.0.0.1"
|
||||
"::1"
|
||||
];
|
||||
ignorecidr = [
|
||||
"10.1.1.0/24"
|
||||
"2a01:e0a:b3a:1dd0::/64"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
streams = {
|
||||
ssh = {
|
||||
cmd = [
|
||||
journalctl
|
||||
"-fn0"
|
||||
"-o"
|
||||
"cat"
|
||||
"-u"
|
||||
"sshd.service"
|
||||
];
|
||||
filters = {
|
||||
failedlogin = {
|
||||
regex = [
|
||||
"authentication failure;.*rhost=<ip>(?: |$)"
|
||||
"Failed password for .* from <ip> port"
|
||||
"Invalid user .* from <ip> "
|
||||
"Connection (?:reset|closed) by invalid user .* <ip> port"
|
||||
];
|
||||
retry = 2;
|
||||
retryperiod = "6h";
|
||||
actions = banFor "ssh" "48h";
|
||||
};
|
||||
connectionreset = {
|
||||
regex = [
|
||||
"Connection (?:reset|closed) by(?: authenticating user .*)? <ip> port"
|
||||
"Received disconnect from <ip> port .*[preauth]"
|
||||
"Timeout before authentication for connection from <ip> to"
|
||||
];
|
||||
retry = 2;
|
||||
retryperiod = "6h";
|
||||
actions = banFor "sshreset" "48h";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
From dc51d7d432eeb408551ae84c6b08d172efbf4649 Mon Sep 17 00:00:00 2001
|
||||
From: ppom <reaction@ppom.me>
|
||||
Date: Tue, 17 Feb 2026 12:00:00 +0100
|
||||
Subject: [PATCH] Add support for macOS
|
||||
|
||||
---
|
||||
src/concepts/plugin.rs | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/src/concepts/plugin.rs b/src/concepts/plugin.rs
|
||||
index c5bc330..8c3c142 100644
|
||||
--- a/src/concepts/plugin.rs
|
||||
+++ b/src/concepts/plugin.rs
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{collections::BTreeMap, io::Error, path, process::Stdio};
|
||||
|
||||
+#[cfg(target_os = "macos")]
|
||||
+use std::os::darwin::fs::MetadataExt;
|
||||
#[cfg(target_os = "freebsd")]
|
||||
use std::os::freebsd::fs::MetadataExt;
|
||||
#[cfg(target_os = "illumos")]
|
||||
--
|
||||
GitLab
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
nixosTests,
|
||||
callPackage,
|
||||
rustPlatform,
|
||||
fetchFromGitLab,
|
||||
|
||||
versionCheckHook,
|
||||
installShellFiles,
|
||||
nix-update-script,
|
||||
|
||||
nixosTests,
|
||||
}:
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "reaction";
|
||||
version = "2.2.1";
|
||||
version = "2.3.0";
|
||||
|
||||
src = fetchFromGitLab {
|
||||
domain = "framagit.org";
|
||||
owner = "ppom";
|
||||
repo = "reaction";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-81i0bkrf86adQWxeZgIoZp/zQQbRJwPqQqZci0ANRFw=";
|
||||
hash = "sha256-OvNJsR9W5MlicqUpr1aOLJ7pI7H7guq1vAlC/hh1Q2o=";
|
||||
};
|
||||
|
||||
cargoHash = "sha256-Bf9XmlY0IMPY4Convftd0Hv8mQbYoiE8WrkkAeaS6Z8=";
|
||||
patches = [
|
||||
# remove patch in next tagged version
|
||||
./add-support-for-macos.patch
|
||||
];
|
||||
|
||||
cargoHash = "sha256-BOFZlVBKf6fjW1L1J8u7Vf+fzNJHlEtQI6YafDjlZ4U=";
|
||||
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
|
||||
@@ -40,13 +48,13 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
||||
# flaky and fails in hydra
|
||||
"--skip=concepts::config::tests::merge_config_distinct_concurrency"
|
||||
];
|
||||
|
||||
cargoTestFlags = [
|
||||
# Skip integration tests for the same reason
|
||||
"--lib"
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
installBin $releaseDir/ip46tables $releaseDir/nft46
|
||||
installManPage $releaseDir/reaction*.1
|
||||
installShellCompletion --cmd reaction \
|
||||
--bash $releaseDir/reaction.bash \
|
||||
@@ -60,17 +68,20 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
||||
versionCheckProgramArg = "--version";
|
||||
doInstallCheck = true;
|
||||
|
||||
passthru.updateScript = nix-update-script { };
|
||||
passthru.tests = { inherit (nixosTests) reaction reaction-firewall; };
|
||||
passthru = {
|
||||
inherit (callPackage ./plugins { }) mkReactionPlugin plugins;
|
||||
updateScript = nix-update-script { };
|
||||
tests = nixosTests.reaction;
|
||||
};
|
||||
|
||||
meta = {
|
||||
changelog = "https://framagit.org/ppom/reaction/-/releases/v${finalAttrs.version}";
|
||||
description = "Scan logs and take action: an alternative to fail2ban";
|
||||
homepage = "https://framagit.org/ppom/reaction";
|
||||
changelog = "https://framagit.org/ppom/reaction/-/releases/v${finalAttrs.version}";
|
||||
license = lib.licenses.agpl3Plus;
|
||||
mainProgram = "reaction";
|
||||
maintainers = with lib.maintainers; [ ppom ];
|
||||
teams = [ lib.teams.ngi ];
|
||||
platforms = lib.platforms.unix;
|
||||
teams = [ lib.teams.ngi ];
|
||||
};
|
||||
})
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
callPackage,
|
||||
reaction,
|
||||
}:
|
||||
{
|
||||
# NOTE: plugins are binaries, so no special integration with the derivation is required
|
||||
# mkReactionPlugin is meant for only official plugins living in the reaction source tree
|
||||
mkReactionPlugin =
|
||||
name: extra:
|
||||
rustPlatform.buildRustPackage (
|
||||
{
|
||||
pname = name;
|
||||
inherit (reaction) version src cargoHash;
|
||||
buildAndTestSubdir = "plugins/${name}";
|
||||
|
||||
meta = {
|
||||
changelog = "https://framagit.org/ppom/reaction/-/releases/v${reaction.version}";
|
||||
description = "Official reaction plugin ${name}";
|
||||
homepage = "https://framagit.org/ppom/reaction";
|
||||
license = lib.licenses.agpl3Plus;
|
||||
mainProgram = name;
|
||||
maintainers = with lib.maintainers; [ ppom ];
|
||||
platforms = lib.platforms.unix;
|
||||
teams = [ lib.teams.ngi ];
|
||||
};
|
||||
}
|
||||
// extra
|
||||
);
|
||||
|
||||
# capture all plugins except default.nix (this file)
|
||||
plugins = lib.removeAttrs (lib.packagesFromDirectoryRecursive {
|
||||
inherit callPackage;
|
||||
directory = ./.;
|
||||
}) [ "default" ];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
ipset,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
reaction,
|
||||
...
|
||||
}:
|
||||
reaction.mkReactionPlugin "reaction-plugin-ipset" {
|
||||
buildInputs = [ ipset ];
|
||||
nativeBuildInputs = [
|
||||
rustPlatform.bindgenHook
|
||||
pkg-config
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
reaction,
|
||||
...
|
||||
}:
|
||||
reaction.mkReactionPlugin "reaction-plugin-virtual" { }
|
||||
@@ -3826,6 +3826,8 @@ with pkgs;
|
||||
|
||||
dprint-plugins = recurseIntoAttrs (callPackage ../by-name/dp/dprint/plugins { });
|
||||
|
||||
reaction-plugins = reaction.passthru.plugins;
|
||||
|
||||
elm2nix = haskell.lib.compose.justStaticExecutables haskellPackages.elm2nix;
|
||||
|
||||
elmPackages = recurseIntoAttrs (callPackage ../development/compilers/elm { });
|
||||
|
||||
Reference in New Issue
Block a user