{ 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;
# If we are running in impure mode we can also enable networked services
impureSetupCommands = let
makeDirs = lib.mapAttrsToList (n: v: ''mkdir C:\Users\${n}\.ssh'') users;
writeKeys = lib.flatten (lib.mapAttrsToList (n: v: builtins.map (key: let
commands = [
''echo "${key}" | Out-File -FilePath C:\Users\${n}\.ssh\authorized_keys''
];
in lib.concatStringsSep "\n" commands) (v.sshKeys or [])) users);
in makeDirs ++ writeKeys ++ [
# TODO: Figure out how to do this step with sandboxing enabled
{
Path = ''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
++ impureSetupCommands
++ 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
pkgs.runCommandNoCC "autounattend.xml" {} ''
${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out
''