diff --git a/chroot-wrapper.bash b/chroot-wrapper.bash new file mode 100644 index 00000000..a8634cd6 --- /dev/null +++ b/chroot-wrapper.bash @@ -0,0 +1,44 @@ +set -e + +# Re-exec ourselves in a private mount namespace so that our bind +# mounts get cleaned up automatically. +if [ -z "$NIXOS_ENTER_REEXEC" ]; then + export NIXOS_ENTER_REEXEC=1 + if [ "$(id -u)" != 0 ]; then + extraFlags="-r" + fi + exec unshare --fork --mount --uts --mount-proc --pid $extraFlags -- "$0" "$@" +else + mount --make-rprivate / +fi + +mountPoint=/mnt + +while [ "$#" -gt 0 ]; do + i="$1"; shift 1 + case "$i" in + --root) + mountPoint="$1"; shift 1 + ;; + --) + command=("$@") + break + ;; + *) + echo "$0: unknown option \`$i'" + exit 1 + ;; + esac +done + +mkdir -p "$mountPoint/dev" "$mountPoint/sys" "$mountPoint/tmp" "$mountPoint/etc" "$mountPoint/proc" +chmod 0755 "$mountPoint/dev" "$mountPoint/sys" "$mountPoint/tmp" "$mountPoint/etc" "$mountPoint/proc" +mount --rbind /dev "$mountPoint/dev" +mount --rbind /sys "$mountPoint/sys" +mount --rbind /proc "$mountPoint/proc" + +touch "$mountPoint/etc/mtab" || true # might be already a working system +(cd "$mountPoint" && ln -snf "../proc/self/mounts" "etc/mtab") # Grub needs an mtab. + +export CHROOTED=1 +exec chroot "$mountPoint" "${command[@]}" diff --git a/flake.nix b/flake.nix index fb393c1d..b943de5a 100644 --- a/flake.nix +++ b/flake.nix @@ -70,10 +70,22 @@ executable = true; destination = "/deploy-rs-activate"; }) + (final.writeTextFile { + name = base.name + "-activate-chroot-wrapper"; + text = '' + # no interpreter: absolute storepaths are not resolvable outside chroot + # run with bash <>, instead + ${nixpkgs.lib.fileContents ./chroot-wrapper.bash}; + ''; + executable = true; + destination = "/deploy-rs-chroot-wrapper"; + }) (final.writeTextFile { name = base.name + "-activate-rs"; text = '' #!${final.runtimeShell} + # required by bin/activate + export PATH="${nixpkgs.lib.makeBinPath [final.nixUnstable final.coreutils]}" exec ${self.defaultPackage.${system}}/bin/activate "$@" ''; executable = true; @@ -84,34 +96,49 @@ }; nixos = base: (custom // { dryActivate = "$PROFILE/bin/switch-to-configuration dry-activate"; }) base.config.system.build.toplevel '' + export PATH="${nixpkgs.lib.makeBinPath [final.coreutils]}" # work around https://github.com/NixOS/nixpkgs/issues/73404 cd /tmp _SYSTEM="/nix/var/nix/profiles/system" - _SWITCH_COMMAND="$PROFILE/bin/switch-to-configuration switch" # always relative to root + _SWITCH_COMMAND="$PROFILE/bin/switch-to-configuration" + + # Set $LOCALE_ARCHIVE to supress some Perl locale warnings. + export LOCALE_ARCHIVE="$PROFILE/sw/lib/locale/locale-archive" _already_on_nixos() { [[ -f "/etc/NIXOS" ]]; } - _ensure_fs_contract() { mkdir -m 0755 -p /etc; touch /etc/NIXOS; } - _insall_bootloader_and_switch() { - ln -sfn /proc/mounts /etc/mtab # Grub needs an mtab. - NIXOS_INSTALL_BOOTLOADER=1 $_SWITCH_COMMAND + _switch_cmd() { + "$_SWITCH_COMMAND" "$1" } - _switch_configuration() { - $_SWITCH_COMMAND + + _become_a_nixos() { touch /etc/NIXOS; } + _if_chrooted() { [[ "''${CHROOTED:-}" == "1" ]] && echo $@; } + _activate_system() { + # Run the activation script. + "$PROFILE/activate" 1>&2 || true + # Cooperatively respect our chrooted environment + ${final.systemd}/bin/systemd-tmpfiles --create --remove $(_if_chrooted "--exclude-prefix=/dev") 1>&2 || true; } if _already_on_nixos then - _switch_configuration + _switch_cmd switch else - _ensure_fs_contract - _insall_bootloader_and_switch + echo "-> Become a NixOS ..." + _become_a_nixos + echo "-> Activate the system ..." + _activate_system + echo "-> Install the boot loader ..." + NIXOS_INSTALL_BOOTLOADER=1 _switch_cmd boot + echo "-> Reboot ..." + $(_if_chrooted sync && echo b |tee /proc/sysrq-trigger) + ${final.systemd}/bin/reboot --reboot fi # https://github.com/serokell/deploy-rs/issues/31 ${with base.config.boot.loader; final.lib.optionalString systemd-boot.enable - "sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"} + "${final.gnused}/bin/sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"} ''; home-manager = base: custom base.activationPackage "$PROFILE/activate"; diff --git a/src/deploy.rs b/src/deploy.rs index 8b6eb705..76fb31f7 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -64,6 +64,12 @@ impl<'a> ActivateCommand<'a> { fn build(self) -> String { let mut cmd = format!("{}/activate-rs", self.closure); + // Exec trough our chroot wrapper that lives within the profile + // down there on the mounted filesystem. + if let Some(mount_point) = self.mount_point { + cmd = format!("bash {0}$(readlink -m {0}{1}/deploy-rs-chroot-wrapper) --root {0} -- {2}", mount_point, self.closure, cmd); + } + if self.debug_logs { cmd = format!("{} --debug-logs", cmd); } @@ -158,6 +164,12 @@ impl<'a> WaitCommand<'a> { fn build(self) -> String { let mut cmd = format!("{}/activate-rs", self.closure); + // Exec trough our chroot wrapper that lives within the profile + // down there on the mounted filesystem. + if let Some(mount_point) = self.mount_point { + cmd = format!("bash {0}$(readlink -m {0}{1}/deploy-rs-chroot-wrapper) --root {0} -- {2}", mount_point, self.closure, cmd); + } + if self.debug_logs { cmd = format!("{} --debug-logs", cmd); } @@ -226,6 +238,12 @@ impl<'a> RevokeCommand<'a> { fn build(self) -> String { let mut cmd = format!("{}/activate-rs", self.closure); + // Exec trough our chroot wrapper that lives within the profile + // down there on the mounted filesystem. + if let Some(mount_point) = self.mount_point { + cmd = format!("bash {0}$(readlink -m {0}{1}/deploy-rs-chroot-wrapper) --root {0} -- {2}", mount_point, self.closure, cmd); + } + if self.debug_logs { cmd = format!("{} --debug-logs", cmd); } @@ -267,6 +285,7 @@ fn test_revoke_command_builder() { } pub struct ConfirmCommand<'a> { + mount_point: Option<&'a str>, sudo: Option<&'a str>, temp_path: &'a str, closure: &'a str, @@ -275,6 +294,7 @@ pub struct ConfirmCommand<'a> { impl<'a> ConfirmCommand<'a> { pub fn from_data(d: &'a data::DeployData) -> Self { ConfirmCommand { + mount_point: d.flags.mount_point.as_deref(), sudo: d.sudo.as_deref(), temp_path: &d.temp_path, closure: &d.profile.profile_settings.path, @@ -285,7 +305,12 @@ impl<'a> ConfirmCommand<'a> { fn build(self) -> String { let lock_path = super::make_lock_path(&self.temp_path, &self.closure); - let mut cmd = format!("rm {}", lock_path); + let mut cmd; + if let Some(mount_point) = self.mount_point { + cmd = format!("rm {}{}", mount_point, lock_path); + } else { + cmd = format!("rm {}", lock_path); + } if let Some(sudo_cmd) = &self.sudo { cmd = format!("{} {}", sudo_cmd, cmd); } diff --git a/src/push.rs b/src/push.rs index ca4f9218..61de975e 100644 --- a/src/push.rs +++ b/src/push.rs @@ -35,6 +35,9 @@ pub enum PushProfileError { #[error("Activation script activate-rs does not exist in profile.\n\ Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")] ActivateRsDoesntExist, + #[error("Activation script helper deploy-rs-chroot-helper does not exist in profile.\n\ + Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")] + DeployRsChrootWrapperDoesntExist, #[error("Failed to run Nix sign command: {0}")] SignError(std::io::Error), #[error("Nix sign command resulted in a bad exit code: {0:?}")] @@ -249,6 +252,10 @@ pub async fn push_profile( return Err(PushProfileError::DeployRsActivateDoesntExist); } + if !Path::new(format!("{}/deploy-rs-chroot-wrapper", closure).as_str()).exists() { + return Err(PushProfileError::DeployRsChrootWrapperDoesntExist); + } + if !Path::new(format!("{}/activate-rs", closure).as_str()).exists() { return Err(PushProfileError::ActivateRsDoesntExist); }