mirror of
https://github.com/nix-community/home-manager.git
synced 2025-11-08 19:46:05 +01:00
radicle: init (#5409)
Co-authored-by: Matthias Beyer <mail@beyermatthias.de> Co-authored-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
parent
1830716059
commit
2aaf924e82
6 changed files with 547 additions and 0 deletions
12
modules/misc/news/2025/09/2025-09-08_20-49-30.nix
Normal file
12
modules/misc/news/2025/09/2025-09-08_20-49-30.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
time = "2025-09-08T18:49:30+00:00";
|
||||
condition = pkgs.stdenv.hostPlatform.isLinux;
|
||||
message = ''
|
||||
A new module is available: 'programs.radicle'.
|
||||
A new service is available: 'services.radicle.node'.
|
||||
|
||||
Radicle is a distributed code forge built on Git.
|
||||
Since it is possible to interact with Radicle storage without running the service, two modules were introduced.
|
||||
'';
|
||||
}
|
||||
269
modules/programs/radicle.nix
Normal file
269
modules/programs/radicle.nix
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
{
|
||||
config,
|
||||
options,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
attrValues
|
||||
filter
|
||||
filterAttrs
|
||||
getExe
|
||||
hasSuffix
|
||||
head
|
||||
id
|
||||
length
|
||||
listToAttrs
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
replaceStrings
|
||||
;
|
||||
|
||||
inherit (lib.types)
|
||||
listOf
|
||||
nullOr
|
||||
str
|
||||
submodule
|
||||
;
|
||||
|
||||
cfg = config.programs.radicle;
|
||||
opt = options.programs.radicle;
|
||||
|
||||
configFile = rec {
|
||||
format = pkgs.formats.json { };
|
||||
name = "config.json";
|
||||
path = pkgs.runCommand name { nativeBuildInputs = [ cfg.cli.package ]; } ''
|
||||
mkdir keys
|
||||
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID/this/is/not/a/real/key/only/a/placeholder" \
|
||||
> keys/radicle.pub
|
||||
cp ${format.generate name cfg.settings} ${name}
|
||||
RAD_HOME=$PWD rad config
|
||||
cp ${name} $out
|
||||
'';
|
||||
};
|
||||
|
||||
publicExplorerSuffix = "$rid$path";
|
||||
in
|
||||
{
|
||||
options = {
|
||||
programs.radicle = {
|
||||
enable = mkEnableOption "Radicle";
|
||||
|
||||
cli.package = mkPackageOption pkgs "radicle-node" { };
|
||||
|
||||
uri = {
|
||||
rad = {
|
||||
browser = {
|
||||
enable = mkOption {
|
||||
description = "Whether to enable `rad:`-URI handling by web browser";
|
||||
default =
|
||||
(hasSuffix publicExplorerSuffix cfg.settings.publicExplorer) && pkgs.stdenv.hostPlatform.isLinux;
|
||||
defaultText = "`true` if a suitable public explorer is detected.";
|
||||
example = false;
|
||||
};
|
||||
preferredNode = mkOption {
|
||||
type = str;
|
||||
description = "The hostname of an instance of `radicle-node`, reachable via HTTPS.";
|
||||
default = "iris.radicle.xyz";
|
||||
example = "radicle-node.example.com";
|
||||
};
|
||||
};
|
||||
vscode = {
|
||||
enable = mkEnableOption "`rad:`-URI handling by VSCode";
|
||||
extension = mkOption {
|
||||
type = str;
|
||||
description = "The unique identifier of the VSCode extension that should handle `rad:`-URIs.";
|
||||
default = "radicle-ide-plugins-team.radicle";
|
||||
};
|
||||
};
|
||||
};
|
||||
web-rad =
|
||||
let
|
||||
detected =
|
||||
let
|
||||
detectionList = attrValues (
|
||||
filterAttrs (n: _: config.programs.${n}.enable) {
|
||||
librewolf = "librewolf.desktop";
|
||||
firefox = "firefox.desktop";
|
||||
chromium = "chromium-browser.desktop";
|
||||
}
|
||||
);
|
||||
in
|
||||
lib.optionals (detectionList == [ ]) (head detectionList);
|
||||
in
|
||||
{
|
||||
enable = mkEnableOption "`web+rad:`-URI handling by web browser";
|
||||
browser = mkOption {
|
||||
description = ''
|
||||
Name of the XDG Desktop Entry for your browser.
|
||||
LibreWolf, Firefox and Chromium configured via home-manager will
|
||||
be detected automatically. The value of this option should likely
|
||||
be the same as the output of
|
||||
`xdg-mime query default x-scheme-handler/https`.
|
||||
'';
|
||||
type = nullOr str;
|
||||
default = detected;
|
||||
defaultText = "Automatically detected browser.";
|
||||
example = "brave.desktop";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
default = { };
|
||||
description = "Radicle configuration, written to `~/.radicle/config.json.";
|
||||
|
||||
type = submodule {
|
||||
freeformType = configFile.format.type;
|
||||
options = {
|
||||
publicExplorer = mkOption {
|
||||
type = str;
|
||||
description = "HTTPS URL pattern used to generate links to view content on Radicle via the browser.";
|
||||
default = "https://app.radicle.xyz/nodes/$host/$rid$path";
|
||||
example = "https://radicle.example.com/nodes/seed.example.com/$rid$path";
|
||||
};
|
||||
|
||||
node = {
|
||||
alias = mkOption {
|
||||
type = str;
|
||||
description = "Human readable alias for your node.";
|
||||
default = config.home.username;
|
||||
defaultText = lib.literalExpression "config.home.username";
|
||||
};
|
||||
listen = mkOption {
|
||||
type = listOf str;
|
||||
description = "Addresses to bind to and listen for inbound connections.";
|
||||
default = [ ];
|
||||
example = [ "127.0.0.1:58776" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
!pkgs.hostPlatform.isLinux
|
||||
-> (
|
||||
(filter id [
|
||||
cfg.uri.rad.browser.enable
|
||||
cfg.uri.rad.vscode.enable
|
||||
]) == [ ]
|
||||
);
|
||||
message = "`rad:`-URI handlers are only supported on Linux.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.uri.web-rad.enable -> cfg.uri.web-rad.browser != null;
|
||||
message = "Could not detect preferred browser. Please set `${builtins.toString opt.uri.web-rad.browser}`.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
1 >= length (
|
||||
filter id [
|
||||
cfg.uri.rad.browser.enable
|
||||
cfg.uri.rad.vscode.enable
|
||||
]
|
||||
);
|
||||
message = "At most one `rad:`-URI handler may be enabled.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.uri.rad.browser.enable -> hasSuffix publicExplorerSuffix cfg.settings.publicExplorer;
|
||||
message = "${opt.uri.rad.browser.enable} is only compatible with a public explorer URL ending in '${publicExplorerSuffix}' but '${cfg.settings.publicExplorer}' does not end with '${publicExplorerSuffix}'.";
|
||||
}
|
||||
];
|
||||
|
||||
home = {
|
||||
packages = [ cfg.cli.package ];
|
||||
file.".radicle/${configFile.name}".source = configFile.path;
|
||||
};
|
||||
|
||||
xdg = {
|
||||
mimeApps.defaultApplications = {
|
||||
"x-scheme-handler/rad" =
|
||||
let
|
||||
isEnabled = cfg.uri.rad.browser.enable || cfg.uri.rad.vscode.enable;
|
||||
|
||||
handlerTarget =
|
||||
if cfg.uri.rad.browser.enable then
|
||||
"rad-to-browser.desktop"
|
||||
else if cfg.uri.rad.vscode.enable then
|
||||
"rad-to-vscode.desktop"
|
||||
else
|
||||
throw "unreachable";
|
||||
|
||||
handler = mkDefault handlerTarget;
|
||||
in
|
||||
mkIf isEnabled handler;
|
||||
|
||||
"x-scheme-handler/web+rad" = mkIf cfg.uri.web-rad.enable (mkDefault cfg.uri.web-rad.browser);
|
||||
};
|
||||
desktopEntries =
|
||||
let
|
||||
mkHandler =
|
||||
{
|
||||
name,
|
||||
shortName,
|
||||
prefix,
|
||||
}:
|
||||
{
|
||||
name = "Open Radicle URIs with ${name}";
|
||||
genericName = "Code Forge";
|
||||
categories = [
|
||||
"Development"
|
||||
"RevisionControl"
|
||||
];
|
||||
exec = getExe (
|
||||
pkgs.writeShellApplication {
|
||||
name = "rad-to-${shortName}";
|
||||
meta.mainProgram = "rad-to-${shortName}";
|
||||
text = ''xdg-open "${prefix}$1"'';
|
||||
}
|
||||
);
|
||||
mimeType = [ "x-scheme-handler/rad" ];
|
||||
noDisplay = true;
|
||||
};
|
||||
|
||||
toHandler = v: {
|
||||
name = "rad-to-${v.shortName}";
|
||||
value = mkIf cfg.uri.rad.${v.shortName}.enable (mkHandler v);
|
||||
};
|
||||
in
|
||||
listToAttrs (
|
||||
map toHandler [
|
||||
{
|
||||
name = "Web Browser";
|
||||
shortName = "browser";
|
||||
prefix =
|
||||
replaceStrings
|
||||
[ "$host" publicExplorerSuffix ]
|
||||
[
|
||||
cfg.uri.rad.browser.preferredNode
|
||||
""
|
||||
]
|
||||
cfg.settings.publicExplorer;
|
||||
}
|
||||
{
|
||||
name = "VSCode";
|
||||
shortName = "vscode";
|
||||
prefix = "vscode://${cfg.uri.rad.vscode.extension}/";
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
lorenzleutgeb
|
||||
matthiasbeyer
|
||||
];
|
||||
}
|
||||
245
modules/services/radicle.nix
Normal file
245
modules/services/radicle.nix
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
generators
|
||||
getBin
|
||||
getExe'
|
||||
last
|
||||
mapAttrsToList
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkMerge
|
||||
mkPackageOption
|
||||
splitString
|
||||
;
|
||||
|
||||
inherit (lib.types)
|
||||
attrsOf
|
||||
nullOr
|
||||
oneOf
|
||||
package
|
||||
path
|
||||
str
|
||||
;
|
||||
|
||||
cfg = config.services.radicle;
|
||||
|
||||
radicleHome = config.home.homeDirectory + "/.radicle";
|
||||
|
||||
gitPath = [ "PATH=${getBin pkgs.gitMinimal}/bin" ];
|
||||
env = attrs: (mapAttrsToList (generators.mkKeyValueDefault { } "=") attrs) ++ gitPath;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.radicle = {
|
||||
node = {
|
||||
enable = mkEnableOption "Radicle Node";
|
||||
package = mkPackageOption pkgs "radicle-node" { };
|
||||
args = mkOption {
|
||||
type = str;
|
||||
description = "Additional command line arguments to pass when executing `radicle-node`.";
|
||||
default = "";
|
||||
example = "--force";
|
||||
};
|
||||
environment = mkOption {
|
||||
type = attrsOf (
|
||||
nullOr (oneOf [
|
||||
str
|
||||
path
|
||||
package
|
||||
])
|
||||
);
|
||||
description = "Environment to set when executing `radicle-node`.";
|
||||
default = { };
|
||||
example = {
|
||||
"RUST_BACKTRACE" = "full";
|
||||
};
|
||||
};
|
||||
lazy = {
|
||||
enable = mkEnableOption "a proxy service to lazily start and stop Radicle Node on demand";
|
||||
exitIdleTime = mkOption {
|
||||
type = str;
|
||||
description = "The idle time after which no interaction with Radicle Node via the `rad` CLI should be stopped, in a format that {manpage}`systemd-socket-proxyd(8)` understands for its `--exit-idle-time` argument.";
|
||||
default = "30min";
|
||||
example = "1h";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.node.enable {
|
||||
systemd.user = {
|
||||
services = {
|
||||
"radicle-node" =
|
||||
let
|
||||
keyFile = name: "${radicleHome}/keys/${name}";
|
||||
keyPair = name: [
|
||||
(keyFile name)
|
||||
(keyFile (name + ".pub"))
|
||||
];
|
||||
radicleKeyPair = keyPair "radicle";
|
||||
in
|
||||
{
|
||||
Unit = {
|
||||
Description = "Radicle Node";
|
||||
Documentation = [
|
||||
"https://radicle.xyz/guides"
|
||||
"man:radicle-node(1)"
|
||||
];
|
||||
StopWhenUnneeded = cfg.node.lazy.enable;
|
||||
ConditionPathExists = radicleKeyPair;
|
||||
};
|
||||
Service = mkMerge ([
|
||||
{
|
||||
Slice = "session.slice";
|
||||
ExecStart = "${getExe' cfg.node.package "radicle-node"} ${cfg.node.args}";
|
||||
Environment = env cfg.node.environment;
|
||||
KillMode = "process";
|
||||
Restart = "no";
|
||||
RestartSec = "2";
|
||||
RestartSteps = "100";
|
||||
RestartMaxDelaySec = "1min";
|
||||
}
|
||||
{
|
||||
# Hardening
|
||||
|
||||
BindPaths = [
|
||||
"${radicleHome}/storage"
|
||||
"${radicleHome}/node"
|
||||
"${radicleHome}/cobs"
|
||||
];
|
||||
|
||||
BindReadOnlyPaths = [
|
||||
"${radicleHome}/config.json"
|
||||
"${radicleHome}/keys"
|
||||
"-/etc/resolv.conf"
|
||||
"/run/systemd"
|
||||
];
|
||||
|
||||
RestrictAddressFamilies = [
|
||||
"AF_UNIX"
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
];
|
||||
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
|
||||
DeviceAllow = ""; # ProtectClock= adds DeviceAllow=char-rtc r
|
||||
KeyringMode = "private";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = "self";
|
||||
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectHome = "tmpfs";
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
|
||||
RuntimeDirectoryMode = "0700";
|
||||
|
||||
SocketBindDeny = [ "any" ];
|
||||
SocketBindAllow = map (
|
||||
addr: "tcp:${last (splitString ":" addr)}"
|
||||
) config.programs.radicle.settings.node.listen;
|
||||
|
||||
StateDirectoryMode = "0750";
|
||||
UMask = "0067";
|
||||
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@aio"
|
||||
"~@chown"
|
||||
"~@keyring"
|
||||
"~@memlock"
|
||||
"~@privileged"
|
||||
"~@resources"
|
||||
"~@setuid"
|
||||
];
|
||||
}
|
||||
]);
|
||||
};
|
||||
"radicle-node-proxy" = mkIf cfg.node.lazy.enable {
|
||||
Unit = {
|
||||
Description = "Radicle Node Proxy";
|
||||
BindsTo = [
|
||||
"radicle-node-proxy.socket"
|
||||
"radicle-node.service"
|
||||
];
|
||||
After = [
|
||||
"radicle-node-proxy.socket"
|
||||
"radicle-node.service"
|
||||
];
|
||||
Documentation = [ "man:systemd-socket-proxyd(8)" ];
|
||||
};
|
||||
Service = {
|
||||
ExecSearchPath = "${pkgs.systemd}/lib/systemd";
|
||||
ExecStart = "systemd-socket-proxyd --exit-idle-time=${cfg.node.lazy.exitIdleTime} %t/radicle-node/proxy.sock";
|
||||
PrivateTmp = "yes";
|
||||
PrivateNetwork = "yes";
|
||||
RuntimeDirectory = "radicle";
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
};
|
||||
};
|
||||
};
|
||||
sockets = mkIf cfg.node.lazy.enable {
|
||||
"radicle-node-control" = {
|
||||
Unit = {
|
||||
Description = "Radicle Node Control Socket";
|
||||
Documentation = [ "man:radicle-node(1)" ];
|
||||
};
|
||||
Socket = {
|
||||
Service = "radicle-node-proxy.service";
|
||||
ListenStream = "%t/radicle-node/control.sock";
|
||||
RuntimeDirectory = "radicle-node";
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
};
|
||||
Install.WantedBy = [ "sockets.target" ];
|
||||
};
|
||||
"radicle-node-proxy" = {
|
||||
Unit = {
|
||||
Description = "Radicle Node Proxy Socket";
|
||||
Documentation = [ "man:systemd-socket-proxyd(8)" ];
|
||||
};
|
||||
Socket = {
|
||||
Service = "radicle-node.service";
|
||||
FileDescriptorName = "control";
|
||||
ListenStream = "%t/radicle-node/proxy.sock";
|
||||
RuntimeDirectory = "radicle-node";
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
};
|
||||
Install.WantedBy = [ "sockets.target" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
programs.radicle.enable = mkDefault true;
|
||||
home.sessionVariables = mkIf cfg.node.lazy.enable {
|
||||
RAD_SOCKET = "\${XDG_RUNTIME_DIR:-/run/user/$UID}/radicle-node/control.sock";
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
lorenzleutgeb
|
||||
matthiasbeyer
|
||||
];
|
||||
}
|
||||
7
tests/modules/programs/radicle/basic-configuration.json
Normal file
7
tests/modules/programs/radicle/basic-configuration.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"node": {
|
||||
"alias": "hm-user",
|
||||
"listen": []
|
||||
},
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path"
|
||||
}
|
||||
13
tests/modules/programs/radicle/basic-configuration.nix
Normal file
13
tests/modules/programs/radicle/basic-configuration.nix
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
programs.radicle.enable = true;
|
||||
|
||||
nmt.script = ''
|
||||
assertFileContent \
|
||||
home-files/.radicle/config.json \
|
||||
${./basic-configuration.json}
|
||||
'';
|
||||
};
|
||||
}
|
||||
1
tests/modules/programs/radicle/default.nix
Normal file
1
tests/modules/programs/radicle/default.nix
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ radicle-basic-configuration = ./basic-configuration.nix; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue