{ pkgs
, lib ? pkgs.lib
, fullName
, organization
, administratorPassword
, uiLanguage ? "en-US"
, inputLocale ? "en-US"
, userLocale ? "en-US"
, systemLocale ? "en-US"
, users ? {}
, productKey ? null
, defaultUser ? null
, setupCommands ? []
, timeZone ? "UTC"
, services ? {}
, impureShellCommands ? []
, ...
}:
let
serviceCommands = lib.mapAttrsToList (
serviceName: attrs: "powershell Set-Service -Name ${serviceName} " + (
lib.concatStringsSep " " (
(
lib.mapAttrsToList (
n: v: if builtins.typeOf v != "bool" then "-${n} ${v}" else "-${n}"
)
) (
# Always run without interaction
{ Force = true; } // attrs
)
)
)
) services;
sshSetupCommands = let
makeDirs = lib.mapAttrsToList (n: v: ''mkdir C:\Users\${n}\.ssh'') users;
writeKeys = lib.flatten (lib.mapAttrsToList (n: v: builtins.map (key: let
commands = [
''powershell.exe Set-Content -Path C:\Users\${n}\.ssh\authorized_keys -Value '${key}' ''
];
in lib.concatStringsSep "\n" commands) (v.sshKeys or [])) users);
mkDirsDesc = builtins.map (c: {Path = c; Description = "Make SSH key dir";}) makeDirs;
writeKeysDesc = builtins.map (c: {Path = c; Description = "Add SSH key";}) writeKeys;
in mkDirsDesc ++ writeKeysDesc ++ [
{
Path = ''powershell.exe Register-PSRepository -Name bootstraprepo -SourceLocation F:\files'';
Description = "Local powershell repo import";
}
{
Path = ''powershell.exe Install-Module -Repository bootstraprepo -Force OpenSSHUtils -Scope AllUsers'';
Description = "Install Openssh.";
}
{
Path = "powershell.exe Start-Service sshd";
Description = "Now start the sshd service.";
}
{
Path = ''powershell.exe Set-Service -Name sshd -StartupType Automatic'';
Description = "Enable SSH by default.";
}
];
assertCommand = c: builtins.typeOf c == "string" || builtins.typeOf c == "set" && builtins.hasAttr "Path" c && builtins.hasAttr "Description" c;
commands = builtins.map (x: assert assertCommand x; if builtins.typeOf x == "string" then { Path = x; Description = x; } else x) (
[
{
Path = "powershell.exe Set-ExecutionPolicy -Force Unrestricted";
Description = "Allow unsigned powershell scripts.";
}
]
++ [
{
Path = ''powershell.exe F:\win-bundle-installer.exe'';
Description = "Install any declared packages.";
}
]
++ setupCommands
++ [
{
Path = ''powershell.exe F:\ssh-setup.ps1'';
Description = "Setup SSH and keys";
}
]
++ serviceCommands
++ impureShellCommands
);
mkCommand = attrs: ''
${lib.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: "<${n}>${v}${n}>") attrs)}
'';
mkCommands = commands: (
builtins.foldl' (
acc: v: rec {
i = acc.i + 1;
values = acc.values ++ [ (mkCommand (v // { Order = builtins.toString i; })) ];
}
) {
i = 0;
values = [];
} commands
).values;
mkUser =
{ name
, password
, description ? ""
, displayName ? ""
, groups ? []
, sshKeys ? [] # Handled in scripts
}: ''
${password}
true
${description}
${displayName}
${builtins.concatStringsSep ";" (lib.unique ([ "Users" ] ++ groups))}
${name}
'';
# Windows expects a flat list of users while we want to manage them as a set
flatUsers = builtins.attrValues (builtins.mapAttrs (name: s: s // { inherit name; }) users);
autounattendXML = pkgs.writeText "autounattend.xml" ''
D:\
E:\
1
EFI
100
2
MSR
16
3
Primary
true
1
FAT32
1
2
2
3
NTFS
C
3
0
true
0
3
/IMAGE/INDEX
1
${if productKey != null then "${productKey}" else ""}
OnError
true
${fullName}
${organization}
${uiLanguage}
${inputLocale}
${systemLocale}
${uiLanguage}
en-US
${userLocale}
${inputLocale}
${systemLocale}
${uiLanguage}
en-US
${userLocale}
true
true
true
true
true
1
${timeZone}
${if administratorPassword != null then ''
${administratorPassword}
true
'' else ""}
${builtins.concatStringsSep "\n" (builtins.map mkUser flatUsers)}
${if defaultUser == null then "" else ''
${(builtins.getAttr defaultUser users).password}
true
true
${defaultUser}
''}
${lib.concatStringsSep "\n" (mkCommands commands)}
0
false
'';
in {
# Lint and format as a sanity check
autounattendXML = pkgs.runCommandNoCC "autounattend.xml" {} ''
${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out
'';
# autounattend.xml is _super_ picky about quotes and other things
setupScript = pkgs.writeText "ssh-setup.ps1" (
''
# Setup SSH and keys
'' +
lib.concatStrings (
builtins.map (c: ''
# ${c.Description}
${c.Path}
'') sshSetupCommands
)
);
}