feat: p4 on hectic-lab

This commit is contained in:
2026-08-31 10:51:38 +00:00
parent 31cac93a4c
commit 07689e1a9e
5 changed files with 422 additions and 5 deletions
+355
View File
@@ -0,0 +1,355 @@
{ ... }:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.p4d;
boolString = value: if value then "1" else "0";
serviceUser = cfg.user;
serviceGroup = cfg.group;
packageExe = if cfg.package != null then "${cfg.package}/bin/p4d" else "/run/current-system/sw/bin/false";
clientExe = if cfg.clientPackage != null then "${cfg.clientPackage}/bin/p4" else "/run/current-system/sw/bin/false";
journalFile = "${cfg.journalDir}/journal";
logFile = "${cfg.dataDir}/logs/p4d.log";
sslPort = if cfg.ssl.enable then "ssl:${cfg.listenAddress}:${toString cfg.port}" else "${cfg.listenAddress}:${toString cfg.port}";
bootstrapPort = "${cfg.bootstrap.listenAddress}:${toString cfg.bootstrap.port}";
typemapLines = [
"binary+l //....uasset"
"binary+l //....umap"
"binary+l //....upk"
"binary+l //....udk"
"binary+l //....ubulk"
"binary+l //....uexp"
"binary+l //....utoc"
"binary+l //....ucas"
"binary+w //....exe"
"binary+w //....dll"
"binary+w //....lib"
"binary+w //....app"
"binary+w //....dylib"
"binary+w //....stub"
"binary+w //....ipa"
"binary+w //....pdb"
] ++ cfg.typemap.extraLines;
typemapSpec = pkgs.writeText "p4d-unreal-typemap.txt" (
lib.concatStringsSep "\n" ([ "Typemap:" ] ++ map (line: "\t${line}") typemapLines) + "\n"
);
mainArgs = [
"-r" cfg.dataDir
"-p" sslPort
"-J" journalFile
"-L" logFile
] ++ lib.optional (!cfg.caseSensitive) "-C1";
bootstrapArgs = [
"-r" cfg.dataDir
"-p" bootstrapPort
"-J" journalFile
"-L" "${cfg.dataDir}/logs/bootstrap.log"
] ++ lib.optional (!cfg.caseSensitive) "-C1";
initScript = pkgs.writeShellScript "p4d-init" ''
set -eu
export P4ROOT=${lib.escapeShellArg cfg.dataDir}
export P4JOURNAL=${lib.escapeShellArg journalFile}
export P4SSLDIR=${lib.escapeShellArg cfg.ssl.dir}
marker_file="$P4ROOT/.hectic-p4d-initialized"
typemap_target="$P4ROOT/unreal-engine.typemap"
server_id_file="$P4ROOT/server.id"
${pkgs.coreutils}/bin/install -d -m 0750 "$P4ROOT"
${pkgs.coreutils}/bin/install -d -m 0750 ${lib.escapeShellArg cfg.journalDir}
${pkgs.coreutils}/bin/install -d -m 0750 ${lib.escapeShellArg cfg.checkpointDir}
${pkgs.coreutils}/bin/install -d -m 0750 "$P4ROOT/logs"
${pkgs.coreutils}/bin/install -d -m 0700 ${lib.escapeShellArg cfg.ssl.dir}
if [ ! -f "$server_id_file" ]; then
umask 077
${pkgs.coreutils}/bin/printf '%s\n' ${lib.escapeShellArg cfg.serverId} > "$server_id_file"
fi
if [ "${boolString cfg.typemap.enable}" = "1" ]; then
${pkgs.coreutils}/bin/install -m 0640 ${lib.escapeShellArg typemapSpec} "$typemap_target"
fi
if [ "${boolString cfg.ssl.enable}" = "1" ] && {
[ ! -f "$P4SSLDIR/privatekey.txt" ] || [ ! -f "$P4SSLDIR/certificate.txt" ];
}; then
${packageExe} -r "$P4ROOT" -Gc
fi
if [ ! -f "$marker_file" ] && [ "${boolString cfg.unicode}" = "1" ] && [ ! -e "$P4ROOT/db.counters" ]; then
${packageExe} ${lib.escapeShellArgs ([ "-r" cfg.dataDir ] ++ lib.optional (!cfg.caseSensitive) "-C1" ++ [ "-xi" ])}
fi
if [ ! -f "$marker_file" ] && [ "${boolString cfg.bootstrap.enable}" = "1" ]; then
temp_pid_file="$P4ROOT/bootstrap.pid"
cleanup() {
if [ -f "$temp_pid_file" ]; then
pid=$(cat "$temp_pid_file")
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
rm -f "$temp_pid_file"
fi
}
trap cleanup EXIT INT TERM
${packageExe} ${lib.escapeShellArgs bootstrapArgs} &
pid=$!
${pkgs.coreutils}/bin/printf '%s\n' "$pid" > "$temp_pid_file"
ready=0
tries=0
while [ "$tries" -lt 30 ]; do
if ${clientExe} -p ${lib.escapeShellArg bootstrapPort} info >/dev/null 2>&1; then
ready=1
break
fi
tries=$((tries + 1))
sleep 1
done
if [ "$ready" -ne 1 ]; then
printf 'p4d bootstrap server failed to become ready on %s\n' ${lib.escapeShellArg bootstrapPort} >&2
exit 1
fi
${lib.optionalString (cfg.securityLevel != null) ''
${clientExe} -p ${lib.escapeShellArg bootstrapPort} configure set security=${toString cfg.securityLevel}
''}
if [ "${boolString cfg.typemap.enable}" = "1" ]; then
${clientExe} -p ${lib.escapeShellArg bootstrapPort} typemap -i < "$typemap_target"
fi
${clientExe} -p ${lib.escapeShellArg bootstrapPort} admin stop || true
wait "$pid" 2>/dev/null || true
rm -f "$temp_pid_file"
trap - EXIT INT TERM
fi
if [ ! -f "$marker_file" ]; then
: > "$marker_file"
fi
'';
in {
options.hectic.services.p4d = {
enable = lib.mkEnableOption "Perforce Helix Core p4d server";
package = lib.mkOption {
type = with lib.types; nullOr package;
default = null;
defaultText = lib.literalExpression "pkgs.p4d";
description = ''
Package providing the `p4d` executable. Left null by default because
nixpkgs marks Perforce packages unfree.
'';
};
clientPackage = lib.mkOption {
type = with lib.types; nullOr package;
default = null;
defaultText = lib.literalExpression "pkgs.p4";
description = ''
Package providing the `p4` client executable used for optional local
bootstrap tasks like typemap installation and security configuration.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "p4d";
description = "System user running the p4d service.";
};
group = lib.mkOption {
type = lib.types.str;
default = "p4d";
description = "System group running the p4d service.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/p4d";
description = "Persistent P4ROOT directory for metadata and archives.";
};
journalDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/p4d/journal";
description = "Directory storing the live journal file.";
};
checkpointDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/p4d/checkpoints";
description = "Directory reserved for checkpoints and offline backups.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "Address p4d listens on for normal client traffic.";
};
port = lib.mkOption {
type = lib.types.port;
default = 1666;
description = "TCP port exposed by p4d.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the p4d TCP port in the firewall.";
};
serverId = lib.mkOption {
type = lib.types.str;
default = "master";
description = "Perforce server identifier written to P4ROOT/server.id.";
};
unicode = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Initialize Unicode mode on first boot. Irreversible after init.";
};
caseSensitive = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether the server remains case-sensitive. Linux default stays true.
Set false only when tooling requires case-insensitive behavior.
'';
};
securityLevel = lib.mkOption {
type = with lib.types; nullOr (ints.between 0 5);
default = 4;
description = ''
Optional `p4 configure set security=<level>` value applied during local
bootstrap. Set null to skip automatic security tuning.
'';
};
ssl = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether p4d listens with SSL enabled.";
};
dir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/p4d/ssl";
description = "Directory holding p4d SSL certificate and key material.";
};
};
bootstrap = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Run one-time local bootstrap tasks on first start: optional security
level configuration and Unreal typemap installation.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Loopback address used by the local temporary bootstrap server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 1667;
description = "Loopback-only port used by the temporary bootstrap server.";
};
};
typemap = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Generate the Unreal-focused typemap spec and optionally install it
during bootstrap.
'';
};
extraLines = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = "Extra lines appended to the generated Perforce typemap spec.";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.package != null;
message = "hectic.services.p4d.package must be set, for example pkgs.p4d.";
}
{
assertion = !cfg.bootstrap.enable || cfg.clientPackage != null;
message = "hectic.services.p4d.clientPackage must be set when bootstrap.enable is true.";
}
];
users.groups.${serviceGroup} = { };
users.users.${serviceUser} = {
isSystemUser = true;
group = serviceGroup;
home = cfg.dataDir;
createHome = true;
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 ${serviceUser} ${serviceGroup} - -"
"d ${cfg.journalDir} 0750 ${serviceUser} ${serviceGroup} - -"
"d ${cfg.checkpointDir} 0750 ${serviceUser} ${serviceGroup} - -"
"d ${cfg.dataDir}/logs 0750 ${serviceUser} ${serviceGroup} - -"
"d ${cfg.ssl.dir} 0700 ${serviceUser} ${serviceGroup} - -"
];
systemd.services.p4d = {
description = "Perforce Helix Core p4d server";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
preStart = builtins.readFile initScript;
serviceConfig = {
Type = "simple";
User = serviceUser;
Group = serviceGroup;
WorkingDirectory = cfg.dataDir;
ExecStart = "${packageExe} ${lib.escapeShellArgs mainArgs}";
Restart = "on-failure";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
UMask = "0077";
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}
+1 -1
View File
@@ -13,7 +13,7 @@ in self.lib.nixpkgs-lib.nixosSystem {
inherit system;
overlays = [ self.overlays.default ];
config.allowUnfreePredicate = pkg:
builtins.elem (inputs.nixpkgs.lib.getName pkg) [ "steamcmd" "steam-unwrapped" ];
builtins.elem (inputs.nixpkgs.lib.getName pkg) [ "steamcmd" "steam-unwrapped" "p4d" "p4" ];
};
modules = [
{ networking.hostName = name; }
+7 -1
View File
@@ -8,7 +8,6 @@
config,
pkgs,
lib,
modulesPath,
...
}:
with builtins;
@@ -99,6 +98,13 @@ in {
workshopItems = [ ];
mods = [ ];
};
services.p4d = {
enable = true;
package = pkgs.p4d;
clientPackage = pkgs.p4;
openFirewall = true;
bootstrap.enable = false;
};
services.gitea-runner-controller = {
# NOTE(yukkop): ephemeral Hetzner VM runners (1 VM = 1 job).
# Runbook: infra/gitea-runners/runbook.md "Ephemeral VM runner cutover".