diff --git a/doc/release-notes/rl-2611.section.md b/doc/release-notes/rl-2611.section.md index 6aa19f0952b7..1df5e6b9a01c 100644 --- a/doc/release-notes/rl-2611.section.md +++ b/doc/release-notes/rl-2611.section.md @@ -19,6 +19,12 @@ - `uhttpmock` providing 0.0 ABI was removed. `uhttpmock_1_0` providing 1.0 ABI was renamed to `uhttpmock` and `uhttpmock_1_0` was kept as an alias. +- The ARMv5 Linux kernel build now uses a standard configuration and generates a standard compressed image instead of the deprecated legacy U‐Boot image format. + `lib.systems.{examples,platforms}.{sheevaplug,pogoplug4}` have been unified into `lib.systems.examples.armv5tel-multiplatform`. + Note that there is no official support for ARMv5 and it is not possible to build even a simple NixOS configuration out of the box. + +- Support for the legacy U‐Boot image format has been removed from the Linux kernel builders, as it is deprecated upstream and no longer used by any platform in Nixpkgs. + - `requireFile` now sets `meta.license = lib.licenses.unfree` by default. Users of `requireFile`-based derivations that preserve this default will need to explicitly allow their evaluation as described in [](#sec-allow-unfree). - `librest` providing 0.7 ABI was removed. `librest_1_0` providing 1.0 ABI was renamed to `librest` and `librest_1_0` was kept as an alias. diff --git a/lib/systems/examples.nix b/lib/systems/examples.nix index 18c954ffc06e..055f727d53c9 100644 --- a/lib/systems/examples.nix +++ b/lib/systems/examples.nix @@ -40,10 +40,9 @@ rec { rust.rustcTarget = "powerpc-unknown-linux-gnu"; }; - sheevaplug = { + armv5tel-multiplatform = { config = "armv5tel-unknown-linux-gnueabi"; - } - // platforms.sheevaplug; + }; raspberryPi = { config = "armv6l-unknown-linux-gnueabihf"; @@ -99,11 +98,6 @@ rec { useLLVM = true; }; - pogoplug4 = { - config = "armv5tel-unknown-linux-gnueabi"; - } - // platforms.pogoplug4; - ben-nanonote = { config = "mipsel-unknown-linux-uclibc"; } diff --git a/lib/systems/platforms.nix b/lib/systems/platforms.nix index ae78b4edc9d4..b10c787728a0 100644 --- a/lib/systems/platforms.nix +++ b/lib/systems/platforms.nix @@ -46,138 +46,15 @@ rec { ## ARM ## - pogoplug4 = { + armv5tel-multiplatform = { linux-kernel = { - name = "pogoplug4"; + name = "armv5tel-multiplatform"; baseConfig = "multi_v5_defconfig"; - autoModules = false; - extraConfig = '' - # Ubi for the mtd - MTD_UBI y - UBIFS_FS y - UBIFS_FS_XATTR y - UBIFS_FS_ADVANCED_COMPR y - UBIFS_FS_LZO y - UBIFS_FS_ZLIB y - UBIFS_FS_DEBUG n - ''; - makeFlags = [ "LOADADDR=0x8000" ]; - target = "uImage"; - # TODO reenable once manual-config's config actually builds a .dtb and this is checked to be working - #DTB = true; - }; - gcc = { - arch = "armv5te"; - }; - }; - - sheevaplug = { - linux-kernel = { - name = "sheevaplug"; - - baseConfig = "multi_v5_defconfig"; - autoModules = false; - extraConfig = '' - BLK_DEV_RAM y - BLK_DEV_INITRD y - BLK_DEV_CRYPTOLOOP m - BLK_DEV_DM m - DM_CRYPT m - MD y - BTRFS_FS m - XFS_FS m - JFS_FS m - EXT4_FS m - USB_STORAGE_CYPRESS_ATACB m - - # mv cesa requires this sw fallback, for mv-sha1 - CRYPTO_SHA1 y - # Fast crypto - CRYPTO_TWOFISH y - CRYPTO_TWOFISH_COMMON y - CRYPTO_BLOWFISH y - CRYPTO_BLOWFISH_COMMON y - - IP_PNP y - IP_PNP_DHCP y - NFS_FS y - ROOT_NFS y - TUN m - NFS_V4 y - NFS_V4_1 y - NFS_FSCACHE y - NFSD m - NFSD_V2_ACL y - NFSD_V3 y - NFSD_V3_ACL y - NFSD_V4 y - NETFILTER y - IP_NF_IPTABLES y - IP_NF_FILTER y - IP_NF_MATCH_ADDRTYPE y - IP_NF_TARGET_LOG y - IP_NF_MANGLE y - IPV6 m - VLAN_8021Q m - - CIFS y - CIFS_XATTR y - CIFS_POSIX y - CIFS_FSCACHE y - CIFS_ACL y - - WATCHDOG y - WATCHDOG_CORE y - ORION_WATCHDOG m - - ZRAM m - NETCONSOLE m - - # Disable OABI to have seccomp_filter (required for systemd) - # https://github.com/raspberrypi/firmware/issues/651 - OABI_COMPAT n - - # Fail to build - DRM n - SCSI_ADVANSYS n - USB_ISP1362_HCD n - SND_SOC n - SND_ALI5451 n - FB_SAVAGE n - SCSI_NSP32 n - ATA_SFF n - SUNGEM n - IRDA n - ATM_HE n - SCSI_ACARD n - BLK_DEV_CMD640_ENHANCED n - - FUSE_FS m - - # systemd uses cgroups - CGROUPS y - - # Latencytop - LATENCYTOP y - - # Ubi for the mtd - MTD_UBI y - UBIFS_FS y - UBIFS_FS_XATTR y - UBIFS_FS_ADVANCED_COMPR y - UBIFS_FS_LZO y - UBIFS_FS_ZLIB y - UBIFS_FS_DEBUG n - - # Kdb, for kernel troubles - KGDB y - KGDB_SERIAL_CONSOLE y - KGDB_KDB y - ''; - makeFlags = [ "LOADADDR=0x0200000" ]; - target = "uImage"; - DTB = true; # Beyond 3.10 + DTB = true; + autoModules = true; + preferBuiltin = true; + target = "zImage"; }; gcc = { arch = "armv5te"; @@ -192,11 +69,6 @@ rec { DTB = true; autoModules = true; preferBuiltin = true; - extraConfig = '' - # Disable OABI to have seccomp_filter (required for systemd) - # https://github.com/raspberrypi/firmware/issues/651 - OABI_COMPAT n - ''; target = "zImage"; }; gcc = { @@ -217,15 +89,6 @@ rec { }; zero-gravitas = { - linux-kernel = { - name = "zero-gravitas"; - - baseConfig = "zero-gravitas_defconfig"; - # Target verified by checking /boot on reMarkable 1 device - target = "zImage"; - autoModules = false; - DTB = true; - }; gcc = { fpu = "neon"; cpu = "cortex-a9"; @@ -233,15 +96,6 @@ rec { }; zero-sugar = { - linux-kernel = { - name = "zero-sugar"; - - baseConfig = "zero-sugar_defconfig"; - DTB = true; - autoModules = false; - preferBuiltin = true; - target = "zImage"; - }; gcc = { cpu = "cortex-a7"; fpu = "neon-vfpv4"; @@ -249,49 +103,6 @@ rec { }; }; - utilite = { - linux-kernel = { - name = "utilite"; - maseConfig = "multi_v7_defconfig"; - autoModules = false; - extraConfig = '' - # Ubi for the mtd - MTD_UBI y - UBIFS_FS y - UBIFS_FS_XATTR y - UBIFS_FS_ADVANCED_COMPR y - UBIFS_FS_LZO y - UBIFS_FS_ZLIB y - UBIFS_FS_DEBUG n - ''; - makeFlags = [ "LOADADDR=0x10800000" ]; - target = "uImage"; - DTB = true; - }; - gcc = { - cpu = "cortex-a9"; - fpu = "neon"; - }; - }; - - guruplug = lib.recursiveUpdate sheevaplug { - # Define `CONFIG_MACH_GURUPLUG' (see - # ) - # and other GuruPlug-specific things. Requires the `guruplug-defconfig' - # patch. - linux-kernel.baseConfig = "guruplug_defconfig"; - }; - - beaglebone = lib.recursiveUpdate armv7l-hf-multiplatform { - linux-kernel = { - name = "beaglebone"; - baseConfig = "bb.org_defconfig"; - autoModules = false; - extraConfig = ""; # TBD kernel config - target = "zImage"; - }; - }; - # https://developer.android.com/ndk/guides/abis#v7a armv7a-android = { linux-kernel.name = "armeabi-v7a"; @@ -305,32 +116,11 @@ rec { armv7l-hf-multiplatform = { linux-kernel = { name = "armv7l-hf-multiplatform"; - Major = "2.6"; # Using "2.6" enables 2.6 kernel syscalls in glibc. - baseConfig = "multi_v7_defconfig"; + baseConfig = "defconfig"; DTB = true; autoModules = true; preferBuiltin = true; target = "zImage"; - extraConfig = '' - # Serial port for Raspberry Pi 3. Wasn't included in ARMv7 defconfig - # until 4.17. - SERIAL_8250_BCM2835AUX y - SERIAL_8250_EXTENDED y - SERIAL_8250_SHARE_IRQ y - - # Hangs ODROID-XU4 - ARM_BIG_LITTLE_CPUIDLE n - - # Disable OABI to have seccomp_filter (required for systemd) - # https://github.com/raspberrypi/firmware/issues/651 - OABI_COMPAT n - - # >=5.12 fails with: - # drivers/net/ethernet/micrel/ks8851_common.o: in function `ks8851_probe_common': - # ks8851_common.c:(.text+0x179c): undefined reference to `__this_module' - # See: https://lore.kernel.org/netdev/20210116164828.40545-1-marex@denx.de/T/ - KS8851_MLL y - ''; }; gcc = { # Some table about fpu flags: @@ -363,22 +153,6 @@ rec { autoModules = true; preferBuiltin = true; extraConfig = '' - # Raspberry Pi 3 stuff. Not needed for s >= 4.10. - ARCH_BCM2835 y - BCM2835_MBOX y - BCM2835_WDT y - RASPBERRYPI_FIRMWARE y - RASPBERRYPI_POWER y - SERIAL_8250_BCM2835AUX y - SERIAL_8250_EXTENDED y - SERIAL_8250_SHARE_IRQ y - - # Cavium ThunderX stuff. - PCI_HOST_THUNDER_ECAM y - - # Nvidia Tegra stuff. - PCI_TEGRA y - # The default (=y) forces us to have the XHCI firmware available in initrd, # which our initrd builder can't currently do easily. USB_XHCI_TEGRA m @@ -412,74 +186,6 @@ rec { }; fuloong2f_n32 = { - linux-kernel = { - name = "fuloong2f_n32"; - baseConfig = "lemote2f_defconfig"; - autoModules = false; - extraConfig = '' - MIGRATION n - COMPACTION n - - # nixos mounts some cgroup - CGROUPS y - - BLK_DEV_RAM y - BLK_DEV_INITRD y - BLK_DEV_CRYPTOLOOP m - BLK_DEV_DM m - DM_CRYPT m - MD y - EXT4_FS m - USB_STORAGE_CYPRESS_ATACB m - - IP_PNP y - IP_PNP_DHCP y - IP_PNP_BOOTP y - NFS_FS y - ROOT_NFS y - TUN m - NFS_V4 y - NFS_V4_1 y - NFS_FSCACHE y - NFSD m - NFSD_V2_ACL y - NFSD_V3 y - NFSD_V3_ACL y - NFSD_V4 y - - # Fail to build - DRM n - SCSI_ADVANSYS n - USB_ISP1362_HCD n - SND_SOC n - SND_ALI5451 n - FB_SAVAGE n - SCSI_NSP32 n - ATA_SFF n - SUNGEM n - IRDA n - ATM_HE n - SCSI_ACARD n - BLK_DEV_CMD640_ENHANCED n - - FUSE_FS m - - # Needed for udev >= 150 - SYSFS_DEPRECATED_V2 n - - VGA_CONSOLE n - VT_HW_CONSOLE_BINDING y - SERIAL_8250_CONSOLE y - FRAMEBUFFER_CONSOLE y - EXT2_FS y - EXT3_FS y - MAGIC_SYSRQ y - - # The kernel doesn't boot at all, with FTRACE - FTRACE n - ''; - target = "vmlinux"; - }; gcc = { arch = "loongson2f"; float = "hard"; @@ -525,35 +231,6 @@ rec { }; }; - # based on: - # https://www.mail-archive.com/qemu-discuss@nongnu.org/msg05179.html - # https://gmplib.org/~tege/qemu.html#mips64-debian - mips64el-qemu-linux-gnuabi64 = { - linux-kernel = { - name = "mips64el"; - baseConfig = "64r2el_defconfig"; - target = "vmlinuz"; - autoModules = false; - DTB = true; - # for qemu 9p passthrough filesystem - extraConfig = '' - MIPS_MALTA y - PAGE_SIZE_4KB y - CPU_LITTLE_ENDIAN y - CPU_MIPS64_R2 y - 64BIT y - CPU_MIPS64_R2 y - - NET_9P y - NET_9P_VIRTIO y - 9P_FS y - 9P_FS_POSIX_ACL y - PCI y - VIRTIO_PCI y - ''; - }; - }; - ## ## Other ## @@ -607,7 +284,7 @@ rec { if version == null then pc else if lib.versionOlder version "6" then - sheevaplug + armv5tel-multiplatform else if lib.versionOlder version "7" then raspberrypi else diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 798e204a578d..bb2058d6b4ac 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -18,10 +18,12 @@ - `boot.vesa` has been removed. It was deprecated in 2020 because Xorg now works better with kernel modesetting. If you still need the legacy VESA 800x600 fallback, set `boot.kernelParams = [ "vga=0x317" "nomodeset" ];` directly. +- Support for the legacy U‐Boot image format has been removed from the initrd generators, as it is deprecated upstream and no longer used by any platform in Nixpkgs. + - Python 2 has been removed from the top-level package set, as it is long past end-of-life. The `python2`, `python27`, `python2Full`, `python27Full`, `python2Packages`, and `python27Packages` attributes, along with the legacy `python`, `pythonFull`, and `pythonPackages` aliases, now throw an error directing you to `python3`. The `isPy2` and `isPy27` package flags have been removed accordingly. The only remaining Python 2 interpreter is vendored inside the `resholve` package for its `oil` dependency and is not exposed for general use. ## Other Notable Changes {#sec-release-26.11-notable-changes} -- Create the first release note entry in this section! +- The `newuidmap` and `newgidmap` security wrappers are now installed with `cap_setuid`/`cap_setgid` file capabilities instead of the setuid-root bit, matching shadow's `--with-fcaps` install mode and other major distributions. Rootless containers (podman, docker-rootless, unprivileged user namespaces) are unaffected. The only behavioural change is that mapping host uid 0 via `/etc/subuid` (which NixOS never configures by default) additionally requires `cap_setfcap`; users who explicitly grant uid 0 in a subuid range can restore the previous behaviour with `security.wrappers.newuidmap.capabilities = lib.mkForce "cap_setuid,cap_setfcap+ep";`. diff --git a/nixos/lib/test-driver/src/test_driver/machine/__init__.py b/nixos/lib/test-driver/src/test_driver/machine/__init__.py index 790da705cf4f..db96445af912 100644 --- a/nixos/lib/test-driver/src/test_driver/machine/__init__.py +++ b/nixos/lib/test-driver/src/test_driver/machine/__init__.py @@ -1031,6 +1031,7 @@ class QemuMachine(BaseMachine): As soon as we read some data from the socket here, we assume that our root shell is operational. """ + assert self.shell (ready, _, _) = select.select([self.shell], [], [], timeout_secs) return bool(ready) diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix index fb1f5612ad25..8fd268ee0d2d 100644 --- a/nixos/modules/installer/tools/tools.nix +++ b/nixos/modules/installer/tools/tools.nix @@ -314,6 +314,27 @@ in name = "nixos-rebuild"; package = config.system.build.nixos-rebuild; }) + ( + { config, ... }: + { + options.system.tools.nixos-rebuild.enableRun0Elevation = lib.mkEnableOption '' + support for being targeted by `nixos-rebuild --elevate=run0 + --ask-elevate-password`. + + This enables polkit and adds {command}`polkit-stdin-agent` to + {option}`environment.systemPackages` so that a deploying host + can find a target-architecture agent at + {file}`/sw/bin/polkit-stdin-agent` after copying the + closure (which is required for cross-architecture deploys and + mismatched nixpkgs revisions to work). + ''; + + config = lib.mkIf config.system.tools.nixos-rebuild.enableRun0Elevation { + security.polkit.enable = lib.mkDefault true; + environment.systemPackages = [ pkgs.polkit-stdin-agent ]; + }; + } + ) (mkToolModule { name = "nixos-version"; package = nixos-version; diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix index d179283ac559..3c53531d90f7 100644 --- a/nixos/modules/programs/shadow.nix +++ b/nixos/modules/programs/shadow.nix @@ -267,13 +267,22 @@ in group = "root"; inherit source; }; + mkCapRoot = capabilities: source: { + inherit capabilities source; + owner = "root"; + group = "root"; + }; in { su = mkSetuidRoot "${config.security.shadow.su.package}/bin/su"; sg = mkSetuidRoot "${cfg.package.out}/bin/sg"; newgrp = mkSetuidRoot "${cfg.package.out}/bin/newgrp"; - newuidmap = mkSetuidRoot "${cfg.package.out}/bin/newuidmap"; - newgidmap = mkSetuidRoot "${cfg.package.out}/bin/newgidmap"; + # File capabilities instead of setuid root, mirroring shadow's + # own --with-fcaps install mode and what Arch/Fedora/Debian ship. + # The kernel only requires CAP_SETUID/CAP_SETGID over the parent + # userns to write a multi-line /proc//[ug]id_map. + newuidmap = mkCapRoot "cap_setuid+ep" "${cfg.package.out}/bin/newuidmap"; + newgidmap = mkCapRoot "cap_setgid+ep" "${cfg.package.out}/bin/newgidmap"; } // lib.optionalAttrs config.users.mutableUsers { chsh = mkSetuidRoot "${cfg.package.out}/bin/chsh"; diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix index 8fd274276a61..338fc1911c01 100644 --- a/nixos/modules/system/activation/activation-script.nix +++ b/nixos/modules/system/activation/activation-script.nix @@ -322,6 +322,9 @@ in description = "Run user-specific NixOS activation"; script = config.system.userActivationScripts.script; unitConfig.ConditionUser = "!@system"; + # switch-to-configuration restarts this explicitly on every switch. + restartIfChanged = false; + serviceConfig.RemainAfterExit = true; serviceConfig.Type = "oneshot"; wantedBy = [ "default.target" ]; }; diff --git a/nixos/modules/virtualisation/nspawn-container/run-nspawn/package.nix b/nixos/modules/virtualisation/nspawn-container/run-nspawn/package.nix index 4f7a0e68748b..9177cd54825d 100644 --- a/nixos/modules/virtualisation/nspawn-container/run-nspawn/package.nix +++ b/nixos/modules/virtualisation/nspawn-container/run-nspawn/package.nix @@ -3,10 +3,10 @@ e2fsprogs, iproute2, lib, - mypy, ruff, setuptools, systemd, + ty, }: buildPythonApplication { @@ -35,13 +35,13 @@ buildPythonApplication { doCheck = true; nativeCheckInputs = [ - mypy ruff + ty ]; checkPhase = '' - echo -e "\x1b[32m## run mypy\x1b[0m" - mypy run_nspawn + echo -e "\x1b[32m## run ty\x1b[0m" + ty check --error-on-warning run_nspawn echo -e "\x1b[32m## run ruff check\x1b[0m" ruff check . echo -e "\x1b[32m## run ruff format\x1b[0m" diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 01d0c6cca8c8..4fe5ed1e56c0 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -551,7 +551,7 @@ in y = 768; }; description = '' - The resolution of the virtual machine display. + The resolution of the virtual machine display (relevant only if virtualised machine uses grub bootloader). ''; }; @@ -1379,7 +1379,6 @@ in "-device usb-tablet,bus=usb-bus.0" ]) (mkIf pkgs.stdenv.hostPlatform.isAarch [ - "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet" diff --git a/nixos/tests/nixos-rebuild-target-host.nix b/nixos/tests/nixos-rebuild-target-host.nix index c37c4ce2ee76..ae872a205a0e 100644 --- a/nixos/tests/nixos-rebuild-target-host.nix +++ b/nixos/tests/nixos-rebuild-target-host.nix @@ -22,6 +22,8 @@ }; system.includeBuildDependencies = true; + # Needed so the offline build of the target config succeeds. + system.extraDependencies = [ pkgs.polkit-stdin-agent ]; virtualisation = { cores = 2; @@ -49,6 +51,11 @@ users.users.alice.extraGroups = [ "wheel" ]; users.users.bob.extraGroups = [ "wheel" ]; + # Needed for --elevate=run0. NixOS's default polkit admin rule is + # `unix-group:wheel`, so bob (in wheel) can authenticate with his + # own password via polkit-stdin-agent. + system.tools.nixos-rebuild.enableRun0Elevation = true; + # Disable sudo for root to ensure sudo isn't called without `--sudo` security.sudo.extraRules = lib.mkForce [ { @@ -142,6 +149,7 @@ deployer.copy_from_host("${configFile "config-1-deployed"}", "/root/configuration-1.nix") deployer.copy_from_host("${configFile "config-2-deployed"}", "/root/configuration-2.nix") deployer.copy_from_host("${configFile "config-3-deployed"}", "/root/configuration-3.nix") + deployer.copy_from_host("${configFile "config-4-deployed"}", "/root/configuration-4.nix") deployer.copy_from_host("${targetNetworkJSON}", "/root/target-network.json") deployer.copy_from_host("${targetConfigJSON}", "/root/target-configuration.json") @@ -168,6 +176,20 @@ target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname").rstrip() assert target_hostname == "config-3-deployed", f"{target_hostname=}" + with subtest("Deploy to bob@target with run0 and password"): + # polkit-stdin-agent registers an agent for systemd-run on the + # target and answers the PAM conversation with the password we + # supply locally. The agent is resolved on the target from + # /sw/bin (see Run0Elevator._remote_agent_argv). + deployer.send_chars("nixos-rebuild switch -I nixos-config=/root/configuration-4.nix --target-host bob@target --elevate=run0 --ask-elevate-password\n") + deployer.wait_until_tty_matches("1", "\\[run0\\] password for bob@target") + deployer.send_chars("${nodes.target.users.users.bob.password}\n") + deployer.wait_until_tty_matches("1", "Done. The new configuration is /nix/store/.*config-4-deployed") + target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname").rstrip() + assert target_hostname == "config-4-deployed", f"{target_hostname=}" + # The target-arch agent is reachable at the stable sw/bin path. + target.succeed("test -x /run/current-system/sw/bin/polkit-stdin-agent") + with subtest("Deploy works with very long TMPDIR"): tmp_dir = "/var/folder/veryveryveryveryverylongpathnamethatdoesnotworkwithcontrolpath" deployer.succeed(f"mkdir -p {tmp_dir}") diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 480d284ada1d..256c25108593 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -739,6 +739,22 @@ in ''; }; + # As above, but with reloadIfChanged: pass 2 must reload, not + # restart. + userServiceMigratedToNixosReloadOnly.configuration = { + imports = [ userServiceMigratedToNixosNoStop.configuration ]; + systemd.user.services.migrated = { + reloadIfChanged = true; + serviceConfig.ExecReload = "${pkgs.coreutils}/bin/true"; + }; + }; + + # As above, but with restartIfChanged = false: pass 2 must skip it. + userServiceMigratedToNixosNoRestart.configuration = { + imports = [ userServiceMigratedToNixosNoStop.configuration ]; + systemd.user.services.migrated.restartIfChanged = false; + }; + no_inhibitors.configuration.system.switch.inhibitors = lib.mkForce { }; inhibitors.configuration.system.switch.inhibitors = lib.mkForce { @@ -810,6 +826,15 @@ in RemainAfterExit=true ExecStart=${pkgs.runtimeShell} -c 'echo home > %t/migrated-owner' ''; + + # Unit file placed in ~/.local/share/systemd/user (lower priority than + # /etc) to simulate a package-shipped unit. + dataMigratedUnit = pkgs.writeText "migrated.service" '' + [Service] + Type=oneshot + RemainAfterExit=true + ExecStart=${pkgs.runtimeShell} -c 'echo data > %t/migrated-owner' + ''; in # python '' @@ -1729,9 +1754,10 @@ in out = switch_to_specialisation("${machine}", "simpleUserService") user_systemctl("is-active usertest.service") - # No-op switch does nothing + # No-op switch leaves the test unit alone. out = switch_to_specialisation("${machine}", "simpleUserService") - assert_lacks(out, "user units:") + assert_lacks(out, "usertest.service") + assert_contains(out, "restarting the following user units: nixos-activation.service") # Modifying the unit stop-starts it (default stopIfChanged=true) out = switch_to_specialisation("${machine}", "simpleUserServiceModified") @@ -1748,7 +1774,7 @@ in # reloadIfChanged=true reloads instead out = switch_to_specialisation("${machine}", "simpleUserServiceReload") assert_lacks(out, "stopping the following user units:") - assert_lacks(out, "restarting the following user units:") + assert_lacks(out, "restarting the following user units: usertest.service") assert_contains(out, "reloading the following user units: usertest.service") user_systemctl("is-active usertest.service") @@ -1818,6 +1844,59 @@ in out = machine.succeed(f"sudo -u usertest {user_env} cat /run/user/1001/migrated-owner") assert_contains(out, "nixos") + # Pass 2 must honour reloadIfChanged. + switch_to_specialisation("${machine}", "") + machine.fail(f"sudo -u usertest {user_env} systemctl --user is-active migrated.service") + seed_home_unit() + out = switch_to_specialisation("${machine}", "userServiceMigratedToNixosReloadOnly") + assert_lacks(out, "restarting (post-activation) the following user units: migrated.service") + assert_contains(out, "reloading (post-activation) the following user units: migrated.service") + user_systemctl("is-active migrated.service") + # Reloaded only, so the home ExecStart never re-ran. + out = machine.succeed(f"sudo -u usertest {user_env} cat /run/user/1001/migrated-owner") + assert_contains(out, "home") + + # Pass 2 must honour restartIfChanged = false. + switch_to_specialisation("${machine}", "") + machine.fail(f"sudo -u usertest {user_env} systemctl --user is-active migrated.service") + seed_home_unit() + out = switch_to_specialisation("${machine}", "userServiceMigratedToNixosNoRestart") + assert_lacks(out, "\nrestarting (post-activation) the following user units: migrated.service") + assert_contains(out, "NOT restarting (post-activation) the following user units: migrated.service") + user_systemctl("is-active migrated.service") + out = machine.succeed(f"sudo -u usertest {user_env} cat /run/user/1001/migrated-owner") + assert_contains(out, "home") + + # Migration from a lower-priority search-path entry ($XDG_DATA_HOME + # here, standing in for ~/.nix-profile/share etc.). /etc outranks + # these, so pass 2 must restart onto the /etc definition. + switch_to_specialisation("${machine}", "") + machine.fail(f"sudo -u usertest {user_env} systemctl --user is-active migrated.service") + machine.succeed( + "sudo -u usertest mkdir -p ~usertest/.local/share/systemd/user", + "sudo -u usertest cp ${dataMigratedUnit} ~usertest/.local/share/systemd/user/migrated.service", + ) + user_systemctl("daemon-reload") + user_systemctl("start migrated.service") + user_systemctl("is-active migrated.service") + out = machine.succeed(f"sudo -u usertest {user_env} cat /run/user/1001/migrated-owner") + assert_contains(out, "data") + out = user_systemctl("show -p FragmentPath migrated.service") + assert_contains(out, "/.local/share/systemd/user/migrated.service") + out = switch_to_specialisation("${machine}", "userServiceMigratedShadowed") + assert_contains(out, "restarting (post-activation) the following user units: migrated.service") + user_systemctl("is-active migrated.service") + out = user_systemctl("show -p FragmentPath migrated.service") + assert_contains(out, "/etc/systemd/user/migrated.service") + out = machine.succeed(f"sudo -u usertest {user_env} cat /run/user/1001/migrated-owner") + assert_contains(out, "nixos") + # Switching again must NOT touch it: /etc already had it, so it is + # not a candidate even though the lower-priority copy is still there. + out = switch_to_specialisation("${machine}", "userServiceMigratedShadowed") + assert_lacks(out, "migrated.service") + machine.succeed("sudo -u usertest rm -rf ~usertest/.local/share/systemd") + user_systemctl("daemon-reload") + # Units that remain shadowed by ~/.config must be left alone in both # passes even though /etc now also defines them. switch_to_specialisation("${machine}", "") diff --git a/nixos/tests/user-activation-scripts.nix b/nixos/tests/user-activation-scripts.nix index e8ea2d05c465..f43332af033f 100644 --- a/nixos/tests/user-activation-scripts.nix +++ b/nixos/tests/user-activation-scripts.nix @@ -13,12 +13,14 @@ isNormalUser = true; }; systemd.user.tmpfiles.users.alice.rules = [ "r %h/file-to-remove" ]; + specialisation.changed.configuration.system.userActivationScripts.bar = "true"; }; testScript = '' def verify_user_activation_run_count(n): - machine.succeed( - '[[ "$(find /home/alice/ -name user-activation-ran.\\* | wc -l)" == %s ]]' % n + t.assertEqual( + n, + int(machine.succeed('find /home/alice/ -name user-activation-ran.\\* | wc -l').rstrip()) ) @@ -36,5 +38,12 @@ machine.succeed("/run/current-system/bin/switch-to-configuration test") verify_user_activation_run_count(2) machine.succeed("[[ ! -f /home/alice/file-to-remove ]] || false") + # Activation must not be killed while running. + machine.fail("journalctl -b _SYSTEMD_USER_UNIT=nixos-activation.service | grep -q 'code=killed'") + + # Changed activation script: still exactly one run. + machine.succeed("/run/current-system/specialisation/changed/bin/switch-to-configuration test") + verify_user_activation_run_count(3) + machine.fail("journalctl -b _SYSTEMD_USER_UNIT=nixos-activation.service | grep -q 'code=killed'") ''; } diff --git a/pkgs/build-support/kernel/initrd-compressor-meta.nix b/pkgs/build-support/kernel/initrd-compressor-meta.nix index 52e009967b50..728b9291627f 100644 --- a/pkgs/build-support/kernel/initrd-compressor-meta.nix +++ b/pkgs/build-support/kernel/initrd-compressor-meta.nix @@ -1,18 +1,15 @@ rec { cat = { executable = pkgs: "cat"; - ubootName = "none"; extension = ".cpio"; }; gzip = { executable = pkgs: "${pkgs.gzip}/bin/gzip"; defaultArgs = [ "-9n" ]; - ubootName = "gzip"; extension = ".gz"; }; bzip2 = { executable = pkgs: "${pkgs.bzip2}/bin/bzip2"; - ubootName = "bzip2"; extension = ".bz2"; }; xz = { @@ -29,24 +26,20 @@ rec { "--check=crc32" "--lzma1=dict=512KiB" ]; - ubootName = "lzma"; extension = ".lzma"; }; lz4 = { executable = pkgs: "${pkgs.lz4}/bin/lz4"; defaultArgs = [ "-l" ]; - ubootName = "lz4"; extension = ".lz4"; }; lzop = { executable = pkgs: "${pkgs.lzop}/bin/lzop"; - ubootName = "lzo"; extension = ".lzo"; }; zstd = { executable = pkgs: "${pkgs.zstd}/bin/zstd"; defaultArgs = [ "-10" ]; - ubootName = "zstd"; extension = ".zst"; }; pigz = gzip // { diff --git a/pkgs/build-support/kernel/make-initrd-ng.nix b/pkgs/build-support/kernel/make-initrd-ng.nix index 768af8f5c48d..8fe1f553c9ed 100644 --- a/pkgs/build-support/kernel/make-initrd-ng.nix +++ b/pkgs/build-support/kernel/make-initrd-ng.nix @@ -4,14 +4,13 @@ let # from it. compressors = import ./initrd-compressor-meta.nix; # Get the basename of the actual compression program from the whole - # compression command, for the purpose of guessing the u-boot + # compression command, for the purpose of guessing the # compression type and filename extension. compressorName = fullCommand: builtins.elemAt (builtins.match "([^ ]*/)?([^ ]+).*" fullCommand) 1; in { stdenvNoCC, cpio, - ubootTools, lib, pkgsBuildHost, makeInitrdNGTool, @@ -57,22 +56,13 @@ in # symlinks to store paths. prepend ? [ ], - # Whether to wrap the initramfs in a u-boot image. - makeUInitrd ? stdenvNoCC.hostPlatform.linux-kernel.target == "uImage", - - # If generating a u-boot image, the architecture to use. The default - # guess may not align with u-boot's nomenclature correctly, so it can - # be overridden. - # See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L81-106 for a list. - uInitrdArch ? stdenvNoCC.hostPlatform.ubootArch, - - # The name of the compression, as recognised by u-boot. - # See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L195-204 for a list. - # If this isn't guessed, you may want to complete the metadata above and send a PR :) - uInitrdCompression ? - _compressorMeta.ubootName - or (throw "Unrecognised compressor ${_compressorName}, please specify uInitrdCompression"), + # Deprecated; remove in 27.05. + makeUInitrd ? null, + uInitrdArch ? null, + uInitrdCompression ? null, }: +assert lib.assertMsg (makeUInitrd == null && uInitrdArch == null && uInitrdCompression == null) + "makeInitrdNg: U‐Boot legacy image support has been removed as it is deprecated upstream and ARMv5 kernels no longer default to uImage"; stdenvNoCC.mkDerivation (finalAttrs: { __structuredAttrs = true; @@ -83,11 +73,8 @@ stdenvNoCC.mkDerivation (finalAttrs: { inherit name extension - makeUInitrd - uInitrdArch prepend ; - ${if makeUInitrd then "uInitrdCompression" else null} = uInitrdCompression; compress = "${_compressorExecutable} ${lib.escapeShellArgs _compressorArgsReal}"; contentsJSON = builtins.toJSON contents; @@ -95,8 +82,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { nativeBuildInputs = [ makeInitrdNGTool cpio - ] - ++ lib.optional makeUInitrd ubootTools; + ]; buildCommand = '' mkdir -p ./root/{run,tmp,var/empty} @@ -109,13 +95,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { done (cd root && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd") - if [ -n "$makeUInitrd" ]; then - mkimage -A "$uInitrdArch" -O linux -T ramdisk -C "$uInitrdCompression" -d "$out/initrd" $out/initrd.img - # Compatibility symlink - ln -sf "initrd.img" "$out/initrd" - else - ln -s "initrd" "$out/initrd$extension" - fi + ln -s "initrd" "$out/initrd$extension" ''; passthru = { diff --git a/pkgs/build-support/kernel/make-initrd.nix b/pkgs/build-support/kernel/make-initrd.nix index 364507ba8e7e..a6624486ffbe 100644 --- a/pkgs/build-support/kernel/make-initrd.nix +++ b/pkgs/build-support/kernel/make-initrd.nix @@ -10,18 +10,16 @@ # of algorithms. let # Some metadata on various compression programs, relevant to naming - # the initramfs file and, if applicable, generating a u-boot image - # from it. + # the initramfs file. compressors = import ./initrd-compressor-meta.nix; # Get the basename of the actual compression program from the whole - # compression command, for the purpose of guessing the u-boot + # compression command, for the purpose of guessing the # compression type and filename extension. compressorName = fullCommand: builtins.elemAt (builtins.match "([^ ]*/)?([^ ]+).*" fullCommand) 1; in { stdenvNoCC, cpio, - ubootTools, lib, pkgsBuildHost, # Name of the derivation (not of the resulting file!) @@ -65,22 +63,13 @@ in # symlinks to store paths. prepend ? [ ], - # Whether to wrap the initramfs in a u-boot image. - makeUInitrd ? stdenvNoCC.hostPlatform.linux-kernel.target or "dummy" == "uImage", - - # If generating a u-boot image, the architecture to use. The default - # guess may not align with u-boot's nomenclature correctly, so it can - # be overridden. - # See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L81-106 for a list. - uInitrdArch ? stdenvNoCC.hostPlatform.linuxArch, - - # The name of the compression, as recognised by u-boot. - # See https://gitlab.denx.de/u-boot/u-boot/-/blob/9bfb567e5f1bfe7de8eb41f8c6d00f49d2b9a426/common/image.c#L195-204 for a list. - # If this isn't guessed, you may want to complete the metadata above and send a PR :) - uInitrdCompression ? - _compressorMeta.ubootName - or (throw "Unrecognised compressor ${_compressorName}, please specify uInitrdCompression"), + # Deprecated; remove in 27.05. + makeUInitrd ? null, + uInitrdArch ? null, + uInitrdCompression ? null, }: +assert lib.assertMsg (makeUInitrd == null && uInitrdArch == null && uInitrdCompression == null) + "makeInitrd: U‐Boot legacy image support has been removed as it is deprecated upstream and ARMv5 kernels no longer default to uImage"; stdenvNoCC.mkDerivation (finalAttrs: { __structuredAttrs = true; @@ -91,18 +80,14 @@ stdenvNoCC.mkDerivation (finalAttrs: { inherit name extension - makeUInitrd - uInitrdArch prepend ; - ${if makeUInitrd then "uInitrdCompression" else null} = uInitrdCompression; builder = ./make-initrd.sh; nativeBuildInputs = [ cpio - ] - ++ lib.optional makeUInitrd ubootTools; + ]; compress = "${_compressorExecutable} ${lib.escapeShellArgs _compressorArgsReal}"; diff --git a/pkgs/build-support/kernel/make-initrd.sh b/pkgs/build-support/kernel/make-initrd.sh index f3bc977279b5..cc30ba0175db 100644 --- a/pkgs/build-support/kernel/make-initrd.sh +++ b/pkgs/build-support/kernel/make-initrd.sh @@ -36,10 +36,4 @@ done (cd root && find * .[^.*] -exec touch -h -d '@1' '{}' +) (cd root && find * .[^.*] -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null | eval -- $compress >> "$out/initrd") -if [ -n "$makeUInitrd" ]; then - mkimage -A "$uInitrdArch" -O linux -T ramdisk -C "$uInitrdCompression" -d "$out/initrd" $out/initrd.img - # Compatibility symlink - ln -sf "initrd.img" "$out/initrd" -else - ln -s "initrd" "$out/initrd$extension" -fi +ln -s "initrd" "$out/initrd$extension" diff --git a/pkgs/by-name/bt/btrfs-progs/package.nix b/pkgs/by-name/bt/btrfs-progs/package.nix index 80f500c9ad30..e5034da09c2e 100644 --- a/pkgs/by-name/bt/btrfs-progs/package.nix +++ b/pkgs/by-name/bt/btrfs-progs/package.nix @@ -21,11 +21,11 @@ stdenv.mkDerivation (finalAttrs: { pname = "btrfs-progs"; - version = "6.19.1"; + version = "7.0"; src = fetchurl { url = "mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${finalAttrs.version}.tar.xz"; - hash = "sha256-uyfh7FTnw8C3suWW+FOnPAej1y8hvJQEIHPCTb8EV5Y="; + hash = "sha256-wobWh2y81yMnoLQX5M/SgDU+wj43tUn9vNeACoMtmpk="; }; nativeBuildInputs = [ diff --git a/pkgs/by-name/ca/capstone/package.nix b/pkgs/by-name/ca/capstone/package.nix index 2f0bb919d37c..b464b9d53885 100644 --- a/pkgs/by-name/ca/capstone/package.nix +++ b/pkgs/by-name/ca/capstone/package.nix @@ -8,13 +8,13 @@ stdenv.mkDerivation (finalAttrs: { pname = "capstone"; - version = "5.0.7"; + version = "5.0.9"; src = fetchFromGitHub { owner = "capstone-engine"; repo = "capstone"; rev = finalAttrs.version; - hash = "sha256-+6QReHZK+iIXspizy6Kvk7cj016HOKgiaKSaP4h7mao="; + hash = "sha256-uAiiKWKGjEATPE0Xc3g+aOLCz5ffIlDmf+7jaGwaZ4I="; }; cmakeFlags = [ diff --git a/pkgs/by-name/dh/dhcpcd/package.nix b/pkgs/by-name/dh/dhcpcd/package.nix index fd0119918706..b7ef86a31b75 100644 --- a/pkgs/by-name/dh/dhcpcd/package.nix +++ b/pkgs/by-name/dh/dhcpcd/package.nix @@ -15,13 +15,13 @@ stdenv.mkDerivation (finalAttrs: { pname = "dhcpcd"; - version = "10.3.1"; + version = "10.3.2"; src = fetchFromGitHub { owner = "NetworkConfiguration"; repo = "dhcpcd"; rev = "v${finalAttrs.version}"; - sha256 = "sha256-L2rR6/qMHWVth2GR3VAoBZmhA6lmCLddbi0VvEG5r70="; + sha256 = "sha256-tJV533j/nQT/PP5KVPJCgTo0Lu8NNMIGnJBvYUG8ufw="; }; nativeBuildInputs = [ pkg-config ]; diff --git a/pkgs/by-name/li/libgit2/package.nix b/pkgs/by-name/li/libgit2/package.nix index bfac9149df06..568fa09201aa 100644 --- a/pkgs/by-name/li/libgit2/package.nix +++ b/pkgs/by-name/li/libgit2/package.nix @@ -22,7 +22,7 @@ stdenv.mkDerivation (finalAttrs: { pname = "libgit2"; - version = "1.9.3"; + version = "1.9.4"; # also check the following packages for updates: python3Packages.pygit2 and libgit2-glib outputs = [ @@ -35,7 +35,7 @@ stdenv.mkDerivation (finalAttrs: { owner = "libgit2"; repo = "libgit2"; tag = "v${finalAttrs.version}"; - hash = "sha256-nJrRdPs86oGNL4W2CJb16oSUgfzYr9A2i5sw9BAehME="; + hash = "sha256-ZKUiz3pdFE2SKxh53X2oyr7hs32Njj5YVA0OXDXz7h0="; }; cmakeFlags = [ diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd index d4f83ac9d803..7ec6def5ca21 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd +++ b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd @@ -21,7 +21,7 @@ nixos-rebuild - reconfigure a NixOS machine _nixos-rebuild_ \[--verbose] [--quiet] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format LOG_FORMAT] [--keep-going] [--keep-failed] [--fallback] [--repair] [--option OPTION OPTION] [--builders BUILDERS] [--include INCLUDE]++ \[--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file] [--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file]++ \[--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT] [--no-build-output] [--use-substitutes] [--help] [--debug] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader]++ - \[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--store-path STORE_PATH] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++ + \[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--store-path STORE_PATH] [--upgrade] [--upgrade-all] [--json] [--elevate {none,sudo,run0}] [--ask-elevate-password] [--no-reexec]++ \[--build-host BUILD_HOST] [--target-host TARGET_HOST] [--no-build-nix] [--image-variant IMAGE_VARIANT]++ \[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}] @@ -269,17 +269,41 @@ It must be one of the following: target-host connection to cache.nixos.org is faster than the connection between hosts. +*--elevate* {none,sudo,run0} + Privilege-elevation method used for activation commands. Setting this + option allows deploying as a non-root user. + + _sudo_ prefixes commands with *sudo*. Additional sudo options can be + passed via the NIX_SUDOOPTS environment variable. + + _run0_ uses systemd's polkit-based elevation. Locally this runs *run0* + directly so the user's normal polkit agent handles any prompts. With + *--target-host* the equivalent _systemd-run --uid=0 --pipe_ form is + used (no remote TTY is allocated). Unless *--ask-elevate-password* is + also passed, the deploying user must be granted the polkit action + _org.freedesktop.systemd1.manage-units_ on the target host without + authentication, e.g. via _security.polkit.extraConfig_. + +*--ask-elevate-password*, *-S* + Prompt locally for a password and feed it to the elevation method. + Implies *--elevate=sudo* if *--elevate* is not given. + + For _sudo_ this uses *sudo --stdin*. For _run0_ the command is wrapped + in *polkit-stdin-agent*, which registers a per-process polkit agent + and answers the PAM conversation from the supplied password. The + machine performing the elevation (the local host, or the target host + with *--target-host*) must set + _system.tools.nixos-rebuild.enableRun0Elevation = true_. + *--elevate=run0 --ask-elevate-password* is not usable otherwise. + *--sudo* - When set, *nixos-rebuild* prefixes activation commands with sudo. - Setting this option allows deploying as a non-root user. + Alias for *--elevate=sudo*. - You can set sudo options by defining the NIX_SUDOOPTS environment - variable. +*--ask-sudo-password* + Alias for *--elevate=sudo --ask-elevate-password*. -*--ask-sudo-password*, *-S* - When set, *nixos-rebuild* will ask for sudo password for remote - activation (i.e.: on *--target-host*) at the start of the build process. - Implies *--sudo*. +*--use-remote-sudo* + Deprecated, use *--elevate=sudo* instead. *--file* _path_, *-f* _path_ Build the NixOS system from the specified file. The file must diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/package.nix b/pkgs/by-name/ni/nixos-rebuild-ng/package.nix index 136962b1b7a9..8112f3d867f8 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/package.nix +++ b/pkgs/by-name/ni/nixos-rebuild-ng/package.nix @@ -1,6 +1,5 @@ { lib, - stdenv, callPackage, installShellFiles, mkShell, diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py index 71480548fb50..c1ad2bde3d00 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py @@ -6,6 +6,7 @@ from typing import Final, assert_never from . import nix, services from .constants import EXECUTABLE, WITH_SHELL_FILES +from .elevate import NO_ELEVATOR, ElevatorKind from .models import Action, BuildAttr, Flake, GroupedNixArgs, Profile from .process import Remote from .utils import LogFormatter @@ -163,18 +164,32 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa help="JSON output, only implemented for 'list-generations' right now", ) main_parser.add_argument( - "--ask-sudo-password", - "-S", - action="store_true", - help="Asks for sudo password for remote activation, implies --sudo", + "--elevate", + choices=ElevatorKind.choices(), + default=None, + help="Privilege-elevation method for activation commands", ) main_parser.add_argument( - "--sudo", action="store_true", help="Prefixes activation commands with sudo" + "--ask-elevate-password", + "-S", + action="store_true", + help="Prompt locally for the password to feed to the elevation " + "method, implies --elevate=sudo if --elevate is not given", + ) + main_parser.add_argument( + "--sudo", + action="store_true", + help="Alias for '--elevate=sudo'", + ) + main_parser.add_argument( + "--ask-sudo-password", + action="store_true", + help="Alias for '--elevate=sudo --ask-elevate-password'", ) main_parser.add_argument( "--use-remote-sudo", action="store_true", - help="Deprecated, use '--sudo' instead", + help="Deprecated, use '--elevate=sudo' instead", ) main_parser.add_argument("--no-ssh-tty", action="store_true", help="Deprecated") main_parser.add_argument( @@ -245,16 +260,32 @@ def parse_args( args.action = Action.DRY_BUILD.value if args.ask_sudo_password: - args.sudo = True + args.ask_elevate_password = True + + if args.use_remote_sudo: + parser_warn("--use-remote-sudo is deprecated, use --elevate=sudo instead") + + # Map the elevate flags onto an Elevator. The password itself is + # attached later via Elevator.with_prompted_password() once the + # target host (used in the prompt) is known. + if args.elevate is not None: + args.elevator = ElevatorKind.from_name(args.elevate) + elif args.sudo or args.use_remote_sudo or args.ask_sudo_password: + args.elevator = ElevatorKind.SUDO.make() + elif args.ask_elevate_password: + # -S historically implied --sudo. Keep that for muscle memory + # but be explicit now that there is more than one backend. + parser_warn( + "--ask-elevate-password without --elevate, falling back to --elevate=sudo" + ) + args.elevator = ElevatorKind.SUDO.make() + else: + args.elevator = NO_ELEVATOR if args.install_grub: parser_warn("--install-grub is deprecated, use --install-bootloader instead") args.install_bootloader = True - if args.use_remote_sudo: - parser_warn("--use-remote-sudo is deprecated, use --sudo instead") - args.sudo = True - if args.fast: parser_warn("--fast is deprecated, use --no-reexec instead") args.no_reexec = True @@ -321,7 +352,7 @@ def execute(argv: list[str]) -> None: args, grouped_nix_args = parse_args(argv) if args.upgrade or args.upgrade_all: - nix.upgrade_channels(args.upgrade_all, args.sudo) + nix.upgrade_channels(args.upgrade_all, args.elevator) action = Action(args.action) # Only run shell scripts from the Nixpkgs tree if the action is @@ -337,8 +368,12 @@ def execute(argv: list[str]) -> None: services.reexec(argv, args, grouped_nix_args) profile = Profile.from_arg(args.profile_name) - target_host = Remote.from_arg(args.target_host, args.ask_sudo_password) - build_host = Remote.from_arg(args.build_host, False, validate_opts=False) + target_host = Remote.from_arg(args.target_host) + build_host = Remote.from_arg(args.build_host, validate_opts=False) + args.elevator = args.elevator.with_prompted_password( + ask=args.ask_elevate_password, + host_label=target_host.host if target_host else "localhost", + ) build_attr = BuildAttr.from_arg(args.attr, args.file) flake = Flake.from_arg(args.flake, target_host) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/elevate.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/elevate.py new file mode 100644 index 000000000000..c69eb097445d --- /dev/null +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/elevate.py @@ -0,0 +1,418 @@ +"""Privilege-elevation backends for activation commands. + +An :class:`Elevator` describes how to wrap a command so it runs as root, +both on the local machine and on a remote target host over SSH (where no +controlling terminal is available), and how to feed it a pre-supplied +password when the backend supports that. ``run_wrapper`` and its callers +carry a single ``elevate: Elevator`` value and let it produce the command +prefix and stdin. + +The remote case has no controlling terminal and the elevated command's +environment depends on the backend (``sudo`` inherits the SSH login env, +while the run0 backend starts a transient unit with only systemd's +default ``PATH``), so each backend builds the full remote argv itself +via :meth:`Elevator.wrap_remote`. +""" + +from __future__ import annotations + +import getpass +import os +import shlex +from abc import ABC, abstractmethod +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field, replace +from enum import Enum +from pathlib import Path, PurePosixPath +from typing import ClassVar, Final, Literal, Self, override + +# Kept here (rather than in process.py) so that elevators can build remote +# argvs without a circular import. +type Arg = str | bytes | os.PathLike[str] | os.PathLike[bytes] +type Args = Sequence[Arg] + + +class _Env(Enum): + PRESERVE_ENV = "PRESERVE" + + @override + def __repr__(self) -> str: + return self.value + + +#: Sentinel meaning "copy this variable from the environment the wrapped +#: command would naturally see" (``os.environ`` locally, the SSH login +#: shell's environment remotely). +PRESERVE_ENV: Final = _Env.PRESERVE_ENV + +type EnvValue = str | Literal[_Env.PRESERVE_ENV] + + +def _remote_env_shell_argv( + prefix: Sequence[str], + env: Mapping[str, EnvValue], + args: Args, +) -> list[Arg]: + """Build `` /bin/sh -c 'exec /usr/bin/env -i K=V… "$@"' sh ``. + + The wrapper runs in the SSH login session, resolves ``PRESERVE_ENV`` + variables against that session's environment, and re-execs the command + with exactly that set. ``/usr/bin/env`` is referenced by absolute path so + the wrapper does not depend on ``PATH`` itself (provided on NixOS via + ``environment.usrbinenv``). + """ + assigns: list[str] = [] + for k, v in env.items(): + if v is PRESERVE_ENV: + assigns.append(f'{k}="${{{k}-}}"') + else: + assigns.append(f"{k}={shlex.quote(v)}") + script = f'exec /usr/bin/env -i {" ".join(assigns)} "$@"' + return [*prefix, "/bin/sh", "-c", script, "sh", *args] + + +@dataclass(frozen=True) +class Wrapped: + """Result of wrapping a command for local elevation.""" + + #: Arguments to prepend to the command. + prefix: list[str] + #: Text to send on the wrapped command's stdin (typically a password + #: followed by a newline), or ``None`` to leave stdin alone. + stdin: str | None = None + + +@dataclass(frozen=True) +class RemoteWrapped: + """Result of wrapping a command for remote elevation over SSH. + + Unlike :class:`Wrapped` this carries the *full* remote argv: backends + differ in where the env-resolution shell wrapper must sit relative to + the elevator (inside ``sudo``, but *around* ``systemd-run``), so a + plain prefix is not expressive enough. + """ + + argv: list[Arg] + stdin: str | None = None + + +class Elevator(ABC): + """How to gain root for activation commands.""" + + #: CLI name, e.g. ``sudo`` or ``run0``. + name: str + + @property + def elevates(self) -> bool: + """Whether this elevator actually changes privileges. + + ``run_wrapper`` uses this to decide between passing ``env`` to + :func:`subprocess.run` directly (unprivileged local case) and + injecting it via ``env -i`` inside the wrapped command (where the + elevator may otherwise scrub the environment). + """ + return True + + @abstractmethod + def wrap_local(self) -> Wrapped: + """Wrap a command run on the local machine.""" + + @abstractmethod + def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped: + """Wrap a command run on a target host over SSH. + + The remote side has no controlling terminal, so backends that need + interactive prompts must either accept a pre-supplied password + (see :meth:`with_password`) or rely on a passwordless policy on + the target. + + *env* is the environment to establish for the elevated command. + :data:`PRESERVE_ENV` values are resolved against the SSH login + shell's environment, and the backend must do so before any step + that replaces it with a service-style one. + """ + + @abstractmethod + def with_password(self, password: str) -> Self: + """Return a copy that will feed *password* to the backend. + + Backends that have no stdin path for credentials must raise + :class:`ElevateError` with a hint pointing at the alternative + (e.g. a polkit rule). + """ + + def with_prompted_password(self, *, ask: bool, host_label: str) -> Self: + """Prompt locally for a password and return a copy carrying it. + + No-op when *ask* is false. May raise :class:`ElevateError` (e.g. + on :class:`NoElevator`). + """ + if not ask: + return self + password = getpass.getpass(f"[{self.name}] password for {host_label}: ") + return self.with_password(password) + + def for_target_config(self, toplevel: PurePosixPath | Path) -> Self: + """Return a copy bound to the toplevel being activated on the target. + + Backends that need a helper binary on the remote + (:class:`Run0Elevator`'s ``polkit-stdin-agent``) use this to find + a target-architecture copy inside the just-copied closure. No-op + by default. + """ + del toplevel # unused in the base implementation + return self + + def on_remote_failure(self) -> str | None: + """Optional hint to print when a remote elevated command fails.""" + return None + + +class ElevateError(Exception): + """Raised for invalid elevator/flag combinations.""" + + +@dataclass(frozen=True) +class NoElevator(Elevator): + name: str = "none" + + @property + @override + def elevates(self) -> bool: + return False + + @override + def wrap_local(self) -> Wrapped: + return Wrapped(prefix=[]) + + @override + def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped: + return RemoteWrapped(argv=_remote_env_shell_argv([], env, args)) + + @override + def with_password(self, password: str) -> Self: + raise ElevateError( + "--ask-elevate-password requires --elevate to select an elevation method" + ) + + +@dataclass(frozen=True) +class SudoElevator(Elevator): + """Wrap with ``sudo``, optionally feeding the password on stdin. + + Extra arguments come from ``NIX_SUDOOPTS`` for backwards + compatibility with the previous implementation in ``run_wrapper``. + """ + + name: str = "sudo" + password: str | None = None + extra_opts: list[str] = field( + default_factory=lambda: shlex.split(os.getenv("NIX_SUDOOPTS", "")) + ) + + @override + def wrap_local(self) -> Wrapped: + # Local sudo can prompt on /dev/tty itself, so the password is + # only piped when one was supplied explicitly. + if self.password is not None: + return Wrapped( + prefix=["sudo", "--prompt=", "--stdin", *self.extra_opts], + stdin=self.password + "\n", + ) + return Wrapped(prefix=["sudo", *self.extra_opts]) + + @override + def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped: + # sudo runs inside the SSH login session, so the env wrapper can + # sit *inside* it and ${VAR-} resolves against the login env. + if self.password is not None: + prefix = ["sudo", "--prompt=", "--stdin", *self.extra_opts] + stdin = self.password + "\n" + else: + prefix = ["sudo", *self.extra_opts] + stdin = None + return RemoteWrapped( + argv=_remote_env_shell_argv(prefix, env, args), + stdin=stdin, + ) + + @override + def with_password(self, password: str) -> Self: + return replace(self, password=password) + + @override + def on_remote_failure(self) -> str | None: + if self.password is None: + return ( + "while running command with remote sudo, did you forget to " + "use --ask-elevate-password?" + ) + return None + + +@dataclass(frozen=True) +class Run0Elevator(Elevator): + """Wrap with systemd's polkit-based ``run0``. + + Locally, ``run0`` is used directly and the user's polkit agent + (graphical or ``pkttyagent``) handles any prompts. + + Remotely we spell out the explicit ``systemd-run --uid=0 --pipe`` + form instead. ``run0`` would internally do the same thing when stdio + is not a TTY (see systemd ``src/run/run.c``), but going through + ``systemd-run`` directly gives us ``--setenv K=V``, which we need to + forward the SSH login environment into the transient unit (whose own + ``PATH`` on NixOS is just the systemd store path), and keeps the + argv independent of whether the SSH session happens to have a TTY. + + Authorisation comes from either a polkit rule on the target granting + ``org.freedesktop.systemd1.manage-units`` to the deploying user, or + from ``polkit-stdin-agent`` for ``--ask-elevate-password``. + """ + + name: str = "run0" + password: str | None = None + #: ``${toplevel}/sw/bin/polkit-stdin-agent`` on the target, set via + #: :meth:`for_target_config`. ``None`` falls back to the target's ``PATH``. + remote_agent: str | None = None + + #: Non-interactive equivalent of ``run0`` (see the class docstring). + REMOTE_BASE: ClassVar[tuple[str, ...]] = ( + "systemd-run", + "--uid=0", + "--pipe", + "--quiet", + "--wait", + "--collect", + "--service-type=exec", + "--send-sighup", + ) + + @override + def wrap_local(self) -> Wrapped: + if self.password is not None: + # Resolved from PATH, same requirement as the remote case: the + # machine doing the elevation needs + # system.tools.nixos-rebuild.enableRun0Elevation. + return Wrapped( + prefix=["polkit-stdin-agent", "--password-fd=0", "--", "run0", "--"], + stdin=self.password + "\n", + ) + return Wrapped(prefix=["run0", "--"]) + + @override + def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped: + # /bin/sh wrapper resolves PRESERVE_ENV in the SSH login session + # and forwards the result into the unit via --setenv. + setenvs: list[str] = [] + for k, v in env.items(): + if v is PRESERVE_ENV: + setenvs.append(f'--setenv={k}="${{{k}-}}"') + else: + setenvs.append(f"--setenv={shlex.quote(f'{k}={v}')}") + script = f'exec {shlex.join(self.REMOTE_BASE)} {" ".join(setenvs)} -- "$@"' + argv: list[Arg] = ["/bin/sh", "-c", script, "sh", *args] + if self.password is not None: + # polkit has no `sudo --stdin` equivalent. polkit-stdin-agent + # registers a per-process agent for the wrapped command and + # answers the PAM conversation from its stdin. + argv = self._remote_agent_argv(argv) + return RemoteWrapped(argv=argv, stdin=self.password + "\n") + return RemoteWrapped(argv=argv) + + #: POSIX sh fragment that picks the first runnable agent from the + #: positional parameters up to ``--`` and execs it with the remainder. + #: ``command -v`` covers both absolute paths and ``PATH`` lookups. + _AGENT_PICKER: ClassVar[str] = ( + "agent=; " + "for a; do " + "shift; " + '[ "$a" = -- ] && break; ' + '[ -z "$agent" ] && command -v "$a" >/dev/null 2>&1 && agent="$a"; ' + "done; " + '[ -n "$agent" ] && exec "$agent" --password-fd=0 -- "$@"; ' + 'echo "nixos-rebuild: polkit-stdin-agent not found on target host ' + '(set system.tools.nixos-rebuild.enableRun0Elevation = true)" >&2; ' + "exit 127" + ) + + def _remote_agent_argv(self, inner: list[Arg]) -> list[Arg]: + """Wrap *inner* in a target-side agent lookup. + + The deployer's own agent may be the wrong arch/nixpkgs (cross-arch + deploys, Darwin deployers, ``--no-reexec``), so resolve on the + target instead: first ``${toplevel}/sw/bin/polkit-stdin-agent`` + (present when ``system.tools.nixos-rebuild.enableRun0Elevation`` + is set), then bare ``polkit-stdin-agent`` on the SSH login PATH. + :data:`_AGENT_PICKER` exits 127 with a hint if neither is found. + """ + candidates: list[str] = [] + if self.remote_agent is not None: + candidates.append(self.remote_agent) + candidates.append("polkit-stdin-agent") + return ["/bin/sh", "-c", self._AGENT_PICKER, "sh", *candidates, "--", *inner] + + @override + def with_password(self, password: str) -> Self: + return replace(self, password=password) + + @override + def for_target_config(self, toplevel: PurePosixPath | Path) -> Self: + return replace( + self, remote_agent=str(toplevel / "sw" / "bin" / "polkit-stdin-agent") + ) + + @override + def on_remote_failure(self) -> str | None: + if self.password is None: + return ( + "while running command with remote run0. Either pass " + "--ask-elevate-password, or grant the deploying user the " + "polkit action 'org.freedesktop.systemd1.manage-units' on " + "the target host (security.polkit.extraConfig)." + ) + return ( + "while running command with remote run0. If the error above " + "mentions polkit-stdin-agent or PolicyKit1, the target host " + "needs system.tools.nixos-rebuild.enableRun0Elevation = true." + ) + + +class ElevatorKind(Enum): + """CLI-selectable elevation backends. + + The enum *value* is the :class:`Elevator` subclass to instantiate, + ``str(member)`` is what ``--elevate`` accepts on the command line. + """ + + NONE = NoElevator + SUDO = SudoElevator + RUN0 = Run0Elevator + + @override + def __str__(self) -> str: + return self.name.lower() + + def make(self) -> Elevator: + """Instantiate a fresh, unparameterised elevator of this kind.""" + cls: type[Elevator] = self.value + return cls() + + @classmethod + def choices(cls) -> list[str]: + """All ``--elevate`` values, for argparse ``choices=``.""" + return [str(m) for m in cls] + + @classmethod + def from_name(cls, name: str) -> Elevator: + try: + return cls[name.upper()].make() + except KeyError: + raise ElevateError( + f"unknown elevation method {name!r}, choose from: " + + ", ".join(cls.choices()) + ) from None + + +#: Singleton used as the default ``elevate=`` argument throughout. +NO_ELEVATOR: Final[Elevator] = NoElevator() diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index c6b5b4d63c45..c9d79868f8f4 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -14,6 +14,7 @@ from textwrap import dedent from typing import Final, Literal from . import tmpdir +from .elevate import NO_ELEVATOR, PRESERVE_ENV, Elevator from .models import ( Action, BuildAttr, @@ -25,7 +26,7 @@ from .models import ( Profile, Remote, ) -from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper +from .process import SSH_DEFAULT_OPTS, run_wrapper from .utils import Args, dict_to_flags FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"] @@ -453,7 +454,7 @@ def get_generations(profile: Profile) -> list[Generation]: def get_generations_from_nix_env( profile: Profile, target_host: Remote | None = None, - sudo: bool = False, + elevate: Elevator = NO_ELEVATOR, ) -> list[Generation]: """Get all NixOS generations from profile with nix-env. Needs root. @@ -468,7 +469,7 @@ def get_generations_from_nix_env( ["nix-env", "-p", profile.path, "--list-generations"], stdout=PIPE, remote=target_host, - sudo=sudo, + elevate=elevate, ) def parse_line(line: str) -> Generation: @@ -600,12 +601,12 @@ def repl_flake(flake: Flake, flake_flags: Args | None = None) -> None: ) -def rollback(profile: Profile, target_host: Remote | None, sudo: bool) -> Path: +def rollback(profile: Profile, target_host: Remote | None, elevate: Elevator) -> Path: "Rollback Nix profile, like one created by `nixos-rebuild switch`." run_wrapper( ["nix-env", "--rollback", "-p", profile.path], remote=target_host, - sudo=sudo, + elevate=elevate, ) # Rollback config PATH is the own profile return profile.path @@ -614,11 +615,11 @@ def rollback(profile: Profile, target_host: Remote | None, sudo: bool) -> Path: def rollback_temporary_profile( profile: Profile, target_host: Remote | None, - sudo: bool, + elevate: Elevator, ) -> Path | None: "Rollback a temporary Nix profile, like one created by `nixos-rebuild test`." generations = get_generations_from_nix_env( - profile, target_host=target_host, sudo=sudo + profile, target_host=target_host, elevate=elevate ) previous_gen_id = None for generation in generations: @@ -635,7 +636,7 @@ def set_profile( profile: Profile, path_to_config: Path, target_host: Remote | None, - sudo: bool, + elevate: Elevator, ) -> None: "Set a path as the current active Nix profile." if not os.environ.get( @@ -668,7 +669,7 @@ def set_profile( run_wrapper( ["nix-env", "-p", profile.path, "--set", path_to_config], remote=target_host, - sudo=sudo, + elevate=elevate, ) @@ -676,7 +677,7 @@ def switch_to_configuration( path_to_config: Path, action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], target_host: Remote | None, - sudo: bool, + elevate: Elevator, install_bootloader: bool = False, specialisation: str | None = None, ) -> None: @@ -716,7 +717,7 @@ def switch_to_configuration( "NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0", }, remote=target_host, - sudo=sudo, + elevate=elevate, # switch-to-configuration is not expected to produce meaningful # stdout, but if it (or any of its children) does, it would leak # into our stdout and break the "only the store path on stdout" @@ -728,7 +729,7 @@ def switch_to_configuration( def upgrade_channels( all_channels: bool = False, - sudo: bool = False, + elevate: Elevator = NO_ELEVATOR, channels_dir: Path = Path("/nix/var/nix/profiles/per-user/root/channels/"), ) -> None: """Upgrade channels for classic Nix. @@ -736,10 +737,10 @@ def upgrade_channels( It will either upgrade just the `nixos` channel (including any channel that has a `.update-on-nixos-rebuild` file) or all. """ - if not sudo and os.geteuid() != 0: + if not elevate.elevates and os.geteuid() != 0: raise NixOSRebuildError( "if you pass the '--upgrade' or '--upgrade-all' flag, you must " - "also pass '--sudo' or run the command as root (e.g., with sudo)" + "also pass '--elevate' or run the command as root" ) channel_updated = False @@ -752,7 +753,7 @@ def upgrade_channels( run_wrapper( ["nix-channel", "--update", channel_path.name], check=False, - sudo=sudo, + elevate=elevate, ) channel_updated = True diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 9f6baaa178a9..d848709ef8f1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -1,17 +1,23 @@ import atexit -import getpass import logging import os import re import shlex import subprocess -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from dataclasses import dataclass -from enum import Enum from ipaddress import AddressValueError, IPv6Address -from typing import Final, Literal, Self, TextIO, TypedDict, Unpack, override +from typing import Final, Self, TextIO, TypedDict, Unpack from . import tmpdir +from .elevate import ( + NO_ELEVATOR, + PRESERVE_ENV, + Arg, + Args, + Elevator, + EnvValue, +) logger: Final = logging.getLogger(__name__) @@ -19,40 +25,22 @@ SSH_DEFAULT_OPTS: Final = [ "-o", "ControlMaster=auto", "-o", - f"ControlPath={tmpdir.TMPDIR_PATH / 'ssh-%n'}", + f"ControlPath={tmpdir.TMPDIR_PATH / 'ssh-%C'}", "-o", "ControlPersist=60", ] -class _Env(Enum): - PRESERVE_ENV = "PRESERVE" - - @override - def __repr__(self) -> str: - return self.value - - -PRESERVE_ENV: Final = _Env.PRESERVE_ENV - - -type Arg = str | bytes | os.PathLike[str] | os.PathLike[bytes] -type Args = Sequence[Arg] -type EnvValue = str | Literal[_Env.PRESERVE_ENV] - - @dataclass(frozen=True) class Remote: host: str opts: list[str] - sudo_password: str | None store_type: str @classmethod def from_arg( cls, host: str | None, - ask_sudo_password: bool | None, validate_opts: bool = True, ) -> Self | None: if not host: @@ -65,25 +53,21 @@ class Remote: opts = shlex.split(os.getenv("NIX_SSHOPTS", "")) if validate_opts: - cls._validate_opts(opts, ask_sudo_password) - sudo_password = None - if ask_sudo_password: - sudo_password = getpass.getpass(f"[sudo] password for {host}: ") - return cls(host, opts, sudo_password, store_type) + cls._validate_opts(opts) + return cls(host, opts, store_type) @staticmethod - def _validate_opts(opts: list[str], ask_sudo_password: bool | None) -> None: + def _validate_opts(opts: list[str]) -> None: for o in opts: if o in ["-t", "-tt", "RequestTTY=yes", "RequestTTY=force"]: logger.warning( f"detected option '{o}' in NIX_SSHOPTS. SSH's TTY may " "cause issues, it is recommended to remove this option" ) - if not ask_sudo_password: - logger.warning( - "if you want to prompt for sudo password use " - "'--ask-sudo-password' option instead" - ) + logger.warning( + "if you want to prompt for a password for remote " + "elevation use '--ask-elevate-password' instead" + ) def ssh_host(self) -> str: """Fix up host string for SSH. @@ -130,7 +114,7 @@ def run_wrapper( env: Mapping[str, EnvValue] | None = None, append_local_env: Mapping[str, str] | None = None, remote: Remote | None = None, - sudo: bool = False, + elevate: Elevator = NO_ELEVATOR, **kwargs: Unpack[RunKwargs], ) -> subprocess.CompletedProcess[str]: "Wrapper around `subprocess.run` that supports extra functionality." @@ -142,27 +126,9 @@ def run_wrapper( resolved_env = _resolve_env_local(normalized_env) if remote: - remote_run_args: list[Arg] = [ - "/bin/sh", - "-c", - _remote_shell_script(normalized_env), - "sh", - *run_args, - ] - - if sudo: - sudo_args = shlex.split(os.getenv("NIX_SUDOOPTS", "")) - if remote.sudo_password: - remote_run_args = [ - "sudo", - "--prompt=", - "--stdin", - *sudo_args, - *remote_run_args, - ] - process_input = remote.sudo_password + "\n" - else: - remote_run_args = ["sudo", *sudo_args, *remote_run_args] + rwrapped = elevate.wrap_remote(normalized_env, run_args) + process_input = rwrapped.stdin + remote_run_args: list[Arg] = rwrapped.argv ssh_args: list[Arg] = [ "ssh", @@ -176,22 +142,19 @@ def run_wrapper( popen_env = None # keep ssh's environment normal else: - if sudo: - # subprocess.run(env=...) would affect sudo, but sudo may drop env - # for the target command. - # So we inject env via `sudo env ... cmd`. + wrapped = elevate.wrap_local() + process_input = wrapped.stdin + if elevate.elevates: + # subprocess.run(env=...) would affect the elevator process, + # which may then drop env for the target command. Inject env + # via `env -i ... cmd` instead so it survives. if env is not None and resolved_env: run_args = _prefix_env_cmd(run_args, resolved_env) - - sudo_args = shlex.split(os.getenv("NIX_SUDOOPTS", "")) - final_args = ["sudo", *sudo_args, *run_args] - - # No need to pass env to subprocess.run; keep sudo's own env - # default. + final_args = [*wrapped.prefix, *run_args] popen_env = None else: - # Non-sudo local: we can fully control the environment with - # subprocess.run(env=...) + # Unprivileged local: we can fully control the environment + # with subprocess.run(env=...) final_args = run_args popen_env = None if env is None else resolved_env @@ -225,16 +188,14 @@ def run_wrapper( return r except KeyboardInterrupt: - # sudo commands are activation only and unlikely to be long running - if remote and not sudo: + # elevated commands are activation only and unlikely to be long + # running + if remote and not elevate.elevates: _kill_long_running_ssh_process(args, remote) raise except subprocess.CalledProcessError: - if sudo and remote and remote.sudo_password is None: - logger.error( - "while running command with remote sudo, did you forget to use " - "--ask-sudo-password?" - ) + if remote and (hint := elevate.on_remote_failure()): + logger.error(hint) raise @@ -268,7 +229,7 @@ def _resolve_env_local(env: dict[str, EnvValue]) -> dict[str, str]: return result -def _prefix_env_cmd(cmd: Sequence[Arg], resolved_env: dict[str, str]) -> list[Arg]: +def _prefix_env_cmd(cmd: Args, resolved_env: dict[str, str]) -> list[Arg]: """ Prefix a command with `env -i K=V ... -- ` to set vars for the command. @@ -280,24 +241,6 @@ def _prefix_env_cmd(cmd: Sequence[Arg], resolved_env: dict[str, str]) -> list[Ar return ["env", "-i", *assigns, *cmd] -def _remote_shell_script(env: Mapping[str, EnvValue]) -> str: - """ - Build the POSIX shell wrapper used for remote execution over SSH. - - SSH sends the remote command as a shell-interpreted command line, so we - need a wrapper to establish a clean environment before `exec`-ing the real - command. This wrapper is always run under `/bin/sh -c` so preserved - variables like `${PATH-}` do not depend on the remote user's login shell. - """ - shell_assigns: list[str] = [] - for k, v in env.items(): - if v is PRESERVE_ENV: - shell_assigns.append(f'{k}="${{{k}-}}"') - else: - shell_assigns.append(f"{k}={shlex.quote(v)}") - return f'exec env -i {" ".join(shell_assigns)} "$@"' - - def _quote_remote_arg(arg: Arg) -> str: return shlex.quote(str(arg)) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index 4de74aa9c4d4..9994941ebb4d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -35,11 +35,7 @@ def reexec( return drv = None - # Parsing the args here but ignore ask_sudo_password since it is not - # needed and we would end up asking sudo password twice - if flake := Flake.from_arg( - args.flake, Remote.from_arg(args.target_host, ask_sudo_password=None) - ): + if flake := Flake.from_arg(args.flake, Remote.from_arg(args.target_host)): drv = nix.build_flake( NIXOS_REBUILD_ATTR, flake, @@ -132,12 +128,12 @@ def _rollback_system( ) -> Path: match action: case Action.SWITCH | Action.BOOT: - path_to_config = nix.rollback(profile, target_host, sudo=args.sudo) + path_to_config = nix.rollback(profile, target_host, elevate=args.elevator) case Action.TEST | Action.BUILD: maybe_path_to_config = nix.rollback_temporary_profile( profile, target_host, - sudo=args.sudo, + elevate=args.elevator, ) if maybe_path_to_config: path_to_config = maybe_path_to_config @@ -231,13 +227,13 @@ def _activate_system( profile, path_to_config, target_host=target_host, - sudo=args.sudo, + elevate=args.elevator, ) nix.switch_to_configuration( path_to_config, action, target_host=target_host, - sudo=args.sudo, + elevate=args.elevator, specialisation=args.specialisation, install_bootloader=args.install_bootloader, ) @@ -247,7 +243,7 @@ def _activate_system( path_to_config, action, target_host=target_host, - sudo=args.sudo, + elevate=args.elevator, specialisation=args.specialisation, install_bootloader=args.install_bootloader, ) @@ -302,6 +298,11 @@ def build_and_activate_system( copy_flags=grouped_nix_args.copy_flags, ) elif args.rollback: + if target_host is not None: + # The elevated `nix-env --rollback` runs before path_to_config + # is known, so point the elevator at the profile to find a + # target-arch helper in the *current* generation's sw/bin. + args.elevator = args.elevator.for_target_config(profile.path) path_to_config = _rollback_system( action=action, args=args, @@ -319,6 +320,11 @@ def build_and_activate_system( grouped_nix_args=grouped_nix_args, ) + if target_host is not None and not args.rollback: + # Prefer the helper from the toplevel we just copied to the + # target (correct arch, independent of re-exec / nixpkgs pin). + args.elevator = args.elevator.for_target_config(path_to_config) + current_config = Path("/run/current-system") if args.diff: if current_config.exists(): diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_elevate.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_elevate.py new file mode 100644 index 000000000000..25a1c3260cda --- /dev/null +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_elevate.py @@ -0,0 +1,155 @@ +from pathlib import PurePosixPath + +import pytest +from pytest import MonkeyPatch + +import nixos_rebuild.elevate as e + + +def test_no_elevator() -> None: + n = e.NoElevator() + assert not n.elevates + assert n.wrap_local() == e.Wrapped(prefix=[]) + rw = n.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + assert rw.stdin is None + assert rw.argv[:2] == ["/bin/sh", "-c"] + assert rw.argv[-1] == "cmd" + with pytest.raises(e.ElevateError): + n.with_password("x") + + +def test_sudo_elevator(monkeypatch: MonkeyPatch) -> None: + monkeypatch.delenv("NIX_SUDOOPTS", raising=False) + + s = e.SudoElevator() + assert s.elevates + assert s.wrap_local() == e.Wrapped(prefix=["sudo"]) + rw = s.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + assert rw.argv[0] == "sudo" + assert rw.argv[1:3] == ["/bin/sh", "-c"] + assert rw.stdin is None + assert s.on_remote_failure() is not None + + sp = s.with_password("hunter2") + rw = sp.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + assert rw.argv[:3] == ["sudo", "--prompt=", "--stdin"] + assert rw.stdin == "hunter2\n" + assert sp.on_remote_failure() is None + # original unchanged + assert s.password is None + + +def test_sudo_elevator_extra_opts(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("NIX_SUDOOPTS", "--preserve-env=FOO -H") + s = e.SudoElevator() + assert s.wrap_local() == e.Wrapped(prefix=["sudo", "--preserve-env=FOO", "-H"]) + rw = s.with_password("p").wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + assert rw.argv[:5] == ["sudo", "--prompt=", "--stdin", "--preserve-env=FOO", "-H"] + assert rw.stdin == "p\n" + + +def test_run0_elevator() -> None: + r = e.Run0Elevator() + assert r.elevates + assert r.wrap_local() == e.Wrapped(prefix=["run0", "--"]) + assert r.on_remote_failure() is not None + + rp = r.with_password("hunter2") + w = rp.wrap_local() + assert w.prefix[0] == "polkit-stdin-agent" + assert "run0" in w.prefix + assert w.stdin == "hunter2\n" + # With a password the failure hint points at the agent, not at -S. + hint = rp.on_remote_failure() + assert hint is not None and "polkit-stdin-agent" in hint + + +def test_run0_elevator_remote() -> None: + r = e.Run0Elevator() + + # No password: /bin/sh wrapper around systemd-run, env passed via + # --setenv so it is resolved in the SSH login shell rather than + # inside the transient unit (which has a useless PATH on NixOS). + rw = r.wrap_remote( + {"PATH": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "1"}, + ["nix-env", "-p", "/profile"], + ) + assert rw.stdin is None + assert rw.argv[:2] == ["/bin/sh", "-c"] + script = rw.argv[2] + assert isinstance(script, str) + assert script.startswith("exec systemd-run --uid=0 --pipe ") + assert '--setenv=PATH="${PATH-}"' in script + assert "--setenv=NIXOS_INSTALL_BOOTLOADER=1" in script + assert script.endswith(' -- "$@"') + assert rw.argv[3:] == ["sh", "nix-env", "-p", "/profile"] + + # With password: an agent-picker /bin/sh wraps the inner /bin/sh, so + # the agent is registered for the inner shell (and the systemd-run it + # execs into). With no toplevel bound the only candidate is bare-name + # PATH lookup. + rw = r.with_password("pw").wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + assert rw.stdin == "pw\n" + assert rw.argv[:3] == ["/bin/sh", "-c", e.Run0Elevator._AGENT_PICKER] + sep = rw.argv.index("--") + assert rw.argv[3:sep] == ["sh", "polkit-stdin-agent"] + assert rw.argv[sep + 1 : sep + 3] == ["/bin/sh", "-c"] + + # Explicit values containing spaces are shell-quoted inside the + # script (the whole thing is later shlex.quoted again for SSH). + rw = r.wrap_remote({"FOO": "a b"}, ["cmd"]) + script = rw.argv[2] + assert isinstance(script, str) + assert "--setenv='FOO=a b'" in script + + +def test_run0_for_target_config() -> None: + toplevel = PurePosixPath("/nix/store/aaaa-nixos-system") + r = e.Run0Elevator().with_password("pw").for_target_config(toplevel) + assert r.remote_agent == f"{toplevel}/sw/bin/polkit-stdin-agent" + + rw = r.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"]) + sep = rw.argv.index("--") + # Order matters: target-arch toplevel first, then PATH. + assert rw.argv[4:sep] == [ + f"{toplevel}/sw/bin/polkit-stdin-agent", + "polkit-stdin-agent", + ] + # Inner argv is preserved verbatim after the separator. + assert rw.argv[sep + 1 : sep + 3] == ["/bin/sh", "-c"] + assert rw.argv[-1] == "cmd" + + # Non-run0 elevators ignore the toplevel. + s = e.SudoElevator() + assert s.for_target_config(toplevel) is s + + +def test_elevator_kind() -> None: + assert isinstance(e.ElevatorKind.from_name("sudo"), e.SudoElevator) + assert isinstance(e.ElevatorKind.from_name("run0"), e.Run0Elevator) + assert isinstance(e.ElevatorKind.from_name("none"), e.NoElevator) + with pytest.raises(e.ElevateError): + e.ElevatorKind.from_name("doas") + assert set(e.ElevatorKind.choices()) == {"none", "sudo", "run0"} + + +def test_with_prompted_password(monkeypatch: MonkeyPatch) -> None: + prompts: list[str] = [] + + def fake_getpass(prompt: str) -> str: + prompts.append(prompt) + return "hunter2" + + monkeypatch.setattr(e.getpass, "getpass", fake_getpass) + + s = e.SudoElevator() + assert s.with_prompted_password(ask=False, host_label="x") is s + assert prompts == [] + + sp = s.with_prompted_password(ask=True, host_label="user@host") + assert isinstance(sp, e.SudoElevator) + assert sp.password == "hunter2" + assert prompts == ["[sudo] password for user@host: "] + + with pytest.raises(e.ElevateError): + e.NoElevator().with_prompted_password(ask=True, host_label="localhost") diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index 3ebba4edc5b6..1e0e5779ac90 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -21,6 +21,37 @@ DEFAULT_RUN_KWARGS = { } +def test_parse_args_elevate() -> None: + r, _ = nr.parse_args(["nixos-rebuild", "switch"]) + assert r.elevator is nr.elevate.NO_ELEVATOR + + r, _ = nr.parse_args(["nixos-rebuild", "switch", "--elevate=sudo"]) + assert isinstance(r.elevator, nr.elevate.SudoElevator) + + r, _ = nr.parse_args(["nixos-rebuild", "switch", "--elevate=run0"]) + assert isinstance(r.elevator, nr.elevate.Run0Elevator) + + # back-compat aliases + for flag in ("--sudo", "--use-remote-sudo"): + r, _ = nr.parse_args(["nixos-rebuild", "switch", flag]) + assert isinstance(r.elevator, nr.elevate.SudoElevator) + + r, _ = nr.parse_args(["nixos-rebuild", "switch", "--ask-sudo-password"]) + assert isinstance(r.elevator, nr.elevate.SudoElevator) + assert r.ask_elevate_password + + # -S without --elevate implies sudo + r, _ = nr.parse_args(["nixos-rebuild", "switch", "-S"]) + assert isinstance(r.elevator, nr.elevate.SudoElevator) + + # explicit --elevate wins over the --sudo alias + r, _ = nr.parse_args(["nixos-rebuild", "switch", "--elevate=none", "--sudo"]) + assert isinstance(r.elevator, nr.elevate.NoElevator) + + with pytest.raises(SystemExit): + nr.parse_args(["nixos-rebuild", "switch", "--elevate=doas"]) + + def test_parse_args() -> None: with pytest.raises(SystemExit) as e: nr.parse_args(["nixos-rebuild", "unknown-action"]) @@ -663,7 +694,7 @@ def test_execute_nix_switch_build_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "mktemp", "-d", @@ -682,7 +713,7 @@ def test_execute_nix_switch_build_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix-store", "--realise", @@ -702,7 +733,7 @@ def test_execute_nix_switch_build_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "readlink", "-f", @@ -720,7 +751,7 @@ def test_execute_nix_switch_build_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "rm", "-rf", @@ -753,7 +784,7 @@ def test_execute_nix_switch_build_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix-env", "-p", @@ -772,7 +803,7 @@ def test_execute_nix_switch_build_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", "-d", @@ -790,7 +821,7 @@ def test_execute_nix_switch_build_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), @@ -879,7 +910,7 @@ def test_execute_nix_switch_flake_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix-env", "-p", @@ -898,7 +929,7 @@ def test_execute_nix_switch_flake_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", "-d", @@ -916,7 +947,7 @@ def test_execute_nix_switch_flake_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), @@ -1004,7 +1035,7 @@ def test_execute_nix_switch_flake_build_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix", "--extra-experimental-features", @@ -1243,7 +1274,7 @@ def test_execute_build_dry_run_build_and_target_remote( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix", "--extra-experimental-features", @@ -1498,7 +1529,7 @@ def test_execute_switch_store_path_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", "-f", @@ -1516,7 +1547,7 @@ def test_execute_switch_store_path_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "nix-env", "-p", @@ -1535,7 +1566,7 @@ def test_execute_switch_store_path_target_host( "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", "-d", @@ -1553,7 +1584,7 @@ def test_execute_switch_store_path_target_host( "sudo", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, str(config_path / "bin/switch-to-configuration"), diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py index 64ffce686976..fa9a41f93a42 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py @@ -81,7 +81,7 @@ def test_flake_parse(mock_node: Mock, tmpdir: Path, monkeypatch: MonkeyPatch) -> autospec=True, return_value=subprocess.CompletedProcess([], 0, stdout="remote\n"), ): - target_host = m.Remote("target@remote", [], None, "ssh") + target_host = m.Remote("target@remote", [], "ssh") assert m.Flake.parse("/path/to/flake", target_host) == m.Flake( "/path/to/flake", 'nixosConfigurations."remote"' ) @@ -201,7 +201,7 @@ def test_flake_from_arg( ), ): assert m.Flake.from_arg( - "/path/to", m.Remote("user@host", [], None, "ssh") + "/path/to", m.Remote("user@host", [], "ssh") ) == m.Flake("/path/to", 'nixosConfigurations."remote-hostname"') diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 9b225eca4e84..8100f396cdb7 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -9,12 +9,15 @@ from unittest.mock import ANY, Mock, call, patch import pytest from pytest import MonkeyPatch +import nixos_rebuild.elevate as e import nixos_rebuild.models as m import nixos_rebuild.nix as n import nixos_rebuild.process as p from .helpers import get_qualified_name +SUDO = e.SudoElevator() + @patch( get_qualified_name(n.run_wrapper, n), @@ -83,7 +86,7 @@ def test_build_flake(mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path) -> def test_build_remote( mock_uuid4: Mock, mock_run: Mock, monkeypatch: MonkeyPatch ) -> None: - build_host = m.Remote("user@host", [], None, "ssh") + build_host = m.Remote("user@host", [], "ssh") monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") def run_wrapper_side_effect( @@ -177,7 +180,7 @@ def test_build_remote_flake( ) -> None: monkeypatch.chdir(tmpdir) flake = m.Flake.parse("/flake.nix#hostname") - build_host = m.Remote("user@host", [], None, "ssh") + build_host = m.Remote("user@host", [], "ssh") monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") assert n.build_remote_flake( @@ -237,9 +240,9 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: n.copy_closure(closure, None) mock_run.assert_not_called() - target_host = m.Remote("user@target.host", [], None, "ssh") - build_host = m.Remote("user@build.host", [], None, "ssh") - target_host_ng = m.Remote("user@target.host", [], None, "ssh-ng") + target_host = m.Remote("user@target.host", [], "ssh") + build_host = m.Remote("user@build.host", [], "ssh") + target_host_ng = m.Remote("user@target.host", [], "ssh-ng") with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: n.copy_closure(closure, target_host) mock_run.assert_called_with( @@ -531,15 +534,15 @@ def test_get_generations_from_nix_env(tmp_path: Path) -> None: ["nix-env", "-p", path, "--list-generations"], stdout=PIPE, remote=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) - remote = m.Remote("user@host", [], "password", "ssh") + remote = m.Remote("user@host", [], "ssh") with patch( get_qualified_name(n.run_wrapper, n), autospec=True, return_value=return_value ) as mock_run: assert n.get_generations_from_nix_env( - m.Profile("system", path), remote, True + m.Profile("system", path), remote, SUDO ) == [ m.Generation(id=2082, current=False, timestamp="2024-11-07 22:58:56"), m.Generation(id=2083, current=False, timestamp="2024-11-07 22:59:41"), @@ -549,7 +552,7 @@ def test_get_generations_from_nix_env(tmp_path: Path) -> None: ["nix-env", "-p", path, "--list-generations"], stdout=PIPE, remote=remote, - sudo=True, + elevate=SUDO, ) @@ -640,19 +643,19 @@ def test_rollback(mock_run: Mock, tmp_path: Path) -> None: profile = m.Profile("system", path) - assert n.rollback(profile, None, False) == profile.path + assert n.rollback(profile, None, e.NO_ELEVATOR) == profile.path mock_run.assert_called_with( ["nix-env", "--rollback", "-p", path], remote=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) - target_host = m.Remote("user@localhost", [], None, "ssh") - assert n.rollback(profile, target_host, True) == profile.path + target_host = m.Remote("user@localhost", [], "ssh") + assert n.rollback(profile, target_host, SUDO) == profile.path mock_run.assert_called_with( ["nix-env", "--rollback", "-p", path], remote=target_host, - sudo=True, + elevate=SUDO, ) @@ -672,7 +675,7 @@ def test_rollback_temporary_profile(tmp_path: Path) -> None: """), ) assert ( - n.rollback_temporary_profile(m.Profile("system", path), None, False) + n.rollback_temporary_profile(m.Profile("system", path), None, e.NO_ELEVATOR) == path.parent / "system-2083-link" ) mock_run.assert_called_with( @@ -684,12 +687,12 @@ def test_rollback_temporary_profile(tmp_path: Path) -> None: ], stdout=PIPE, remote=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) - target_host = m.Remote("user@localhost", [], None, "ssh") + target_host = m.Remote("user@localhost", [], "ssh") assert ( - n.rollback_temporary_profile(m.Profile("foo", path), target_host, True) + n.rollback_temporary_profile(m.Profile("foo", path), target_host, SUDO) == path.parent / "foo-2083-link" ) mock_run.assert_called_with( @@ -701,12 +704,12 @@ def test_rollback_temporary_profile(tmp_path: Path) -> None: ], stdout=PIPE, remote=target_host, - sudo=True, + elevate=SUDO, ) with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: mock_run.return_value = CompletedProcess([], 0, stdout="") - assert n.rollback_temporary_profile(profile, None, False) is None + assert n.rollback_temporary_profile(profile, None, e.NO_ELEVATOR) is None @patch(get_qualified_name(n.run_wrapper, n), autospec=True) @@ -719,25 +722,25 @@ def test_set_profile(mock_run: Mock) -> None: m.Profile("system", profile_path), config_path, target_host=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) mock_run.assert_called_with( ["nix-env", "-p", profile_path, "--set", config_path], remote=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) mock_run.return_value = CompletedProcess([], 1) - with pytest.raises(m.NixOSRebuildError) as e: + with pytest.raises(m.NixOSRebuildError) as exc: n.set_profile( m.Profile("system", profile_path), config_path, target_host=None, - sudo=False, + elevate=e.NO_ELEVATOR, ) - assert str(e.value).startswith( + assert str(exc.value).startswith( "error: your NixOS configuration path seems to be missing essential files." ) @@ -756,7 +759,7 @@ def test_switch_to_configuration_without_systemd_run( n.switch_to_configuration( profile_path, m.Action.SWITCH, - sudo=False, + elevate=e.NO_ELEVATOR, target_host=None, specialisation=None, install_bootloader=False, @@ -764,29 +767,29 @@ def test_switch_to_configuration_without_systemd_run( mock_run.assert_called_with( [profile_path / "bin/switch-to-configuration", "switch"], env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "LOCALE_ARCHIVE": e.PRESERVE_ENV, + "NIXOS_NO_CHECK": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "0", }, - sudo=False, + elevate=e.NO_ELEVATOR, remote=None, stdout=sys.stderr, ) - with pytest.raises(m.NixOSRebuildError) as e: + with pytest.raises(m.NixOSRebuildError) as exc: n.switch_to_configuration( config_path, m.Action.BOOT, - sudo=False, + elevate=e.NO_ELEVATOR, target_host=None, specialisation="special", ) assert ( - str(e.value) + str(exc.value) == "error: '--specialisation' can only be used with 'switch' and 'test'" ) - target_host = m.Remote("user@localhost", [], None, "ssh") + target_host = m.Remote("user@localhost", [], "ssh") with monkeypatch.context() as mp: mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") mp.setenv("PATH", "/path/to/bin") @@ -795,7 +798,7 @@ def test_switch_to_configuration_without_systemd_run( n.switch_to_configuration( Path("/path/to/config"), m.Action.TEST, - sudo=True, + elevate=SUDO, target_host=target_host, install_bootloader=True, specialisation="special", @@ -806,11 +809,11 @@ def test_switch_to_configuration_without_systemd_run( "test", ], env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "LOCALE_ARCHIVE": e.PRESERVE_ENV, + "NIXOS_NO_CHECK": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "1", }, - sudo=True, + elevate=SUDO, remote=target_host, stdout=sys.stderr, ) @@ -830,7 +833,7 @@ def test_switch_to_configuration_with_systemd_run( n.switch_to_configuration( profile_path, m.Action.SWITCH, - sudo=False, + elevate=e.NO_ELEVATOR, target_host=None, specialisation=None, install_bootloader=False, @@ -842,16 +845,16 @@ def test_switch_to_configuration_with_systemd_run( "switch", ], env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "LOCALE_ARCHIVE": e.PRESERVE_ENV, + "NIXOS_NO_CHECK": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "0", }, - sudo=False, + elevate=e.NO_ELEVATOR, remote=None, stdout=sys.stderr, ) - target_host = m.Remote("user@localhost", [], None, "ssh") + target_host = m.Remote("user@localhost", [], "ssh") with monkeypatch.context() as mp: mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") mp.setenv("PATH", "/path/to/bin") @@ -860,7 +863,7 @@ def test_switch_to_configuration_with_systemd_run( n.switch_to_configuration( Path("/path/to/config"), m.Action.TEST, - sudo=True, + elevate=SUDO, target_host=target_host, install_bootloader=True, specialisation="special", @@ -872,11 +875,11 @@ def test_switch_to_configuration_with_systemd_run( "test", ], env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "LOCALE_ARCHIVE": e.PRESERVE_ENV, + "NIXOS_NO_CHECK": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "1", }, - sudo=True, + elevate=SUDO, remote=target_host, stdout=sys.stderr, ) @@ -887,11 +890,13 @@ def test_switch_to_configuration_with_systemd_run( def test_upgrade_channels(mock_run: Mock, mock_geteuid: Mock, tmpdir: Path) -> None: tmp_path = Path(tmpdir) - with pytest.raises(m.NixOSRebuildError) as e: - n.upgrade_channels(all_channels=False, sudo=False, channels_dir=tmp_path) - assert str(e.value) == ( + with pytest.raises(m.NixOSRebuildError) as exc: + n.upgrade_channels( + all_channels=False, elevate=e.NO_ELEVATOR, channels_dir=tmp_path + ) + assert str(exc.value) == ( "error: if you pass the '--upgrade' or '--upgrade-all' flag, you must " - "also pass '--sudo' or run the command as root (e.g., with sudo)" + "also pass '--elevate' or run the command as root" ) (tmp_path / "nixos").mkdir() @@ -899,19 +904,20 @@ def test_upgrade_channels(mock_run: Mock, mock_geteuid: Mock, tmpdir: Path) -> N (tmp_path / "nixos-hardware" / ".update-on-nixos-rebuild").touch() (tmp_path / "home-manager").mkdir() - # should work because we are passing sudo=True even with os.geteuid == 1000 - n.upgrade_channels(all_channels=False, sudo=True, channels_dir=tmp_path) + # should work because we are passing an elevator even with os.geteuid == 1000 + n.upgrade_channels(all_channels=False, elevate=SUDO, channels_dir=tmp_path) + # Path.glob order is filesystem-dependent, so don't assert call order. mock_run.assert_has_calls( [ call( ["nix-channel", "--update", "nixos-hardware"], check=False, - sudo=True, + elevate=SUDO, ), call( ["nix-channel", "--update", "nixos"], check=False, - sudo=True, + elevate=SUDO, ), ], any_order=True, @@ -921,23 +927,23 @@ def test_upgrade_channels(mock_run: Mock, mock_geteuid: Mock, tmpdir: Path) -> N # root check mock_geteuid.return_value = 0 - n.upgrade_channels(all_channels=True, sudo=False, channels_dir=tmp_path) + n.upgrade_channels(all_channels=True, elevate=e.NO_ELEVATOR, channels_dir=tmp_path) mock_run.assert_has_calls( [ call( ["nix-channel", "--update", "home-manager"], check=False, - sudo=False, + elevate=e.NO_ELEVATOR, ), call( ["nix-channel", "--update", "nixos-hardware"], check=False, - sudo=False, + elevate=e.NO_ELEVATOR, ), call( ["nix-channel", "--update", "nixos"], check=False, - sudo=False, + elevate=e.NO_ELEVATOR, ), ], any_order=True, diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py index 3780b21f26d5..2abdd26ebe07 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py @@ -3,28 +3,46 @@ from unittest.mock import patch from pytest import MonkeyPatch +import nixos_rebuild.elevate as e import nixos_rebuild.models as m import nixos_rebuild.process as p -def test_remote_shell_script() -> None: - assert p._remote_shell_script({"PATH": p.PRESERVE_ENV}) == ( - '''exec env -i PATH="${PATH-}" "$@"''' - ) - assert p._remote_shell_script( +def test_remote_env_shell_argv() -> None: + assert e._remote_env_shell_argv([], {"PATH": e.PRESERVE_ENV}, ["cmd"]) == [ + "/bin/sh", + "-c", + '''exec /usr/bin/env -i PATH="${PATH-}" "$@"''', + "sh", + "cmd", + ] + assert e._remote_env_shell_argv( + ["sudo"], { - "PATH": p.PRESERVE_ENV, - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "PATH": e.PRESERVE_ENV, + "LOCALE_ARCHIVE": e.PRESERVE_ENV, + "NIXOS_NO_CHECK": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "0", - } - ) == ( - """exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" """ - '''NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"''' - ) - assert p._remote_shell_script({"PATH": p.PRESERVE_ENV, "FOO": "some value"}) == ( - '''exec env -i PATH="${PATH-}" FOO='some value' "$@"''' - ) + }, + ["cmd", "arg"], + ) == [ + "sudo", + "/bin/sh", + "-c", + """exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" """ + '''NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"''', + "sh", + "cmd", + "arg", + ] + assert e._remote_env_shell_argv( + [], {"PATH": e.PRESERVE_ENV, "FOO": "some value"}, [] + ) == [ + "/bin/sh", + "-c", + '''exec /usr/bin/env -i PATH="${PATH-}" FOO='some value' "$@"''', + "sh", + ] @patch.dict(p.os.environ, {"PATH": "/path/to/bin"}, clear=True) @@ -43,7 +61,7 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "flags"], check=False, - sudo=True, + elevate=e.SudoElevator(), env={"FOO": "bar"}, ) mock_run.assert_called_with( @@ -67,7 +85,6 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "flags"], check=False, - sudo=False, append_local_env={"FOO": "bar"}, ) mock_run.assert_called_with( @@ -89,7 +106,6 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "flags"], check=False, - sudo=False, env={"PATH": "/"}, append_local_env={"FOO": "bar"}, ) @@ -112,7 +128,7 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "some flags"], check=True, - remote=m.Remote("user@localhost", ["--ssh", "opt"], "password", "ssh"), + remote=m.Remote("user@localhost", ["--ssh", "opt"], "ssh"), ) mock_run.assert_called_with( [ @@ -124,7 +140,7 @@ def test_run_wrapper(mock_run: Any) -> None: "--", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", "--with", @@ -140,9 +156,9 @@ def test_run_wrapper(mock_run: Any) -> None: p.run_wrapper( ["test", "--with", "flags"], check=True, - sudo=True, + elevate=e.SudoElevator(password="password"), env={"FOO": "bar"}, - remote=m.Remote("user@localhost", ["--ssh", "opt"], "password", "ssh"), + remote=m.Remote("user@localhost", ["--ssh", "opt"], "ssh"), ) mock_run.assert_called_with( [ @@ -157,7 +173,7 @@ def test_run_wrapper(mock_run: Any) -> None: "--stdin", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" FOO=bar "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" FOO=bar "$@"'""", "sh", "test", "--with", @@ -181,7 +197,7 @@ def test__kill_long_running_ssh_process(mock_run: Any) -> None: "build", "/nix/store/la0c8nmpr9xfclla0n4f3qq9iwgdrq4g-nixos-system-sankyuu-nixos-25.05.20250424.f771eb4.drv^*", ], - m.Remote("user@localhost", opts=[], sudo_password=None, store_type="ssh"), + m.Remote("user@localhost", opts=[], store_type="ssh"), ) mock_run.assert_called_with( [ @@ -204,21 +220,18 @@ def test__kill_long_running_ssh_process(mock_run: Any) -> None: def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("NIX_SSHOPTS", "") - assert m.Remote.from_arg("user@localhost", None, False) == m.Remote( + assert m.Remote.from_arg("user@localhost", validate_opts=False) == m.Remote( "user@localhost", opts=[], - sudo_password=None, store_type="ssh", ) - with patch("getpass.getpass", autospec=True, return_value="password"): - monkeypatch.setenv("NIX_SSHOPTS", "-f foo -b bar -t") - assert m.Remote.from_arg("user@localhost", True, True) == m.Remote( - "user@localhost", - opts=["-f", "foo", "-b", "bar", "-t"], - sudo_password="password", - store_type="ssh", - ) + monkeypatch.setenv("NIX_SSHOPTS", "-f foo -b bar -t") + assert m.Remote.from_arg("user@localhost") == m.Remote( + "user@localhost", + opts=["-f", "foo", "-b", "bar", "-t"], + store_type="ssh", + ) def test_ssh_host() -> None: @@ -240,18 +253,94 @@ def test_ssh_host() -> None: } for host_input, expected in ssh_remotes.items(): - remote = m.Remote.from_arg(host_input, None, False) + remote = m.Remote.from_arg(host_input, validate_opts=False) assert remote is not None assert remote.ssh_host() == expected assert remote.store_type == "ssh" for host_input, expected in ssh_ng_remotes.items(): - remote = m.Remote.from_arg(host_input, None, False) + remote = m.Remote.from_arg(host_input, validate_opts=False) assert remote is not None assert remote.ssh_host() == expected assert remote.store_type == "ssh-ng" +@patch.dict(p.os.environ, {"PATH": "/path/to/bin"}, clear=True) +@patch("subprocess.run", autospec=True) +def test_run_wrapper_run0(mock_run: Any) -> None: + p.run_wrapper(["cmd", "arg"], elevate=e.Run0Elevator()) + mock_run.assert_called_with( + ["run0", "--", "cmd", "arg"], + check=True, + text=True, + errors="surrogateescape", + env=None, + input=None, + ) + + run0_script = ( + "exec systemd-run --uid=0 --pipe --quiet --wait --collect " + "--service-type=exec --send-sighup " + '--setenv=PATH="${PATH-}" -- "$@"' + ) + + p.run_wrapper( + ["cmd", "arg"], + elevate=e.Run0Elevator(), + remote=m.Remote("user@host", [], "ssh"), + ) + mock_run.assert_called_with( + [ + "ssh", + *p.SSH_DEFAULT_OPTS, + "user@host", + "--", + "/bin/sh", + "-c", + p._quote_remote_arg(run0_script), + "sh", + "cmd", + "arg", + ], + check=True, + text=True, + errors="surrogateescape", + env=None, + input=None, + ) + + p.run_wrapper( + ["cmd"], + elevate=e.Run0Elevator().with_password("pw"), + remote=m.Remote("user@host", [], "ssh"), + ) + mock_run.assert_called_with( + [ + "ssh", + *p.SSH_DEFAULT_OPTS, + "user@host", + "--", + "/bin/sh", + "-c", + p._quote_remote_arg(e.Run0Elevator._AGENT_PICKER), + "sh", + # No toplevel bound, so the only candidate is PATH lookup. + "polkit-stdin-agent", + "--", + "/bin/sh", + "-c", + p._quote_remote_arg(run0_script), + "sh", + "cmd", + ], + check=True, + text=True, + errors="surrogateescape", + env=None, + input="pw\n", + ) + + @patch("subprocess.run", autospec=True) def test_custom_sudo_args(mock_run: Any) -> None: with patch.dict( @@ -262,7 +351,7 @@ def test_custom_sudo_args(mock_run: Any) -> None: p.run_wrapper( ["test"], check=False, - sudo=True, + elevate=e.SudoElevator(), ) mock_run.assert_called_with( [ @@ -287,8 +376,8 @@ def test_custom_sudo_args(mock_run: Any) -> None: p.run_wrapper( ["test"], check=False, - sudo=True, - remote=m.Remote("user@localhost", [], None, "ssh"), + elevate=e.SudoElevator(), + remote=m.Remote("user@localhost", [], "ssh"), ) mock_run.assert_called_with( [ @@ -302,7 +391,7 @@ def test_custom_sudo_args(mock_run: Any) -> None: "--args", "/bin/sh", "-c", - """'exec env -i PATH="${PATH-}" "$@"'""", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", "sh", "test", ], diff --git a/pkgs/by-name/po/polkit-stdin-agent/package.nix b/pkgs/by-name/po/polkit-stdin-agent/package.nix new file mode 100644 index 000000000000..7377d1727ca6 --- /dev/null +++ b/pkgs/by-name/po/polkit-stdin-agent/package.nix @@ -0,0 +1,48 @@ +{ + lib, + rustPlatform, + fetchFromGitea, + nix-update-script, + nixosTests, +}: + +rustPlatform.buildRustPackage (finalAttrs: { + pname = "polkit-stdin-agent"; + version = "0.3.0"; + + src = fetchFromGitea { + domain = "codeberg.org"; + owner = "r-vdp"; + repo = "polkit-stdin-agent"; + tag = "v${finalAttrs.version}"; + hash = "sha256-Nl/+IBbUEsxSKSWLXwUB3mV4iAG0z9mv+Bl6CSeFzR4="; + }; + + cargoHash = "sha256-Eb/7ejVmtG5FNSh66gZO3337KCPNi+xtYVC5qyFKJzg="; + + strictDeps = true; + __structuredAttrs = true; + + passthru = { + updateScript = nix-update-script { }; + tests = { inherit (nixosTests) nixos-rebuild-target-host; }; + }; + + meta = { + description = "Non-interactive polkit authentication agent that answers PAM prompts from a file descriptor"; + longDescription = '' + Registers a per-process polkit authentication agent for a wrapped + command and answers the PAM conversation from a file descriptor + instead of /dev/tty, giving run0 / systemd-run the same + "password on stdin" ergonomics as `sudo --stdin`. + + Used by `nixos-rebuild --elevate=run0 --ask-elevate-password` to + authenticate on a target host over SSH without allocating a TTY. + ''; + homepage = "https://codeberg.org/r-vdp/polkit-stdin-agent"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ rvdp ]; + platforms = lib.platforms.linux; + mainProgram = "polkit-stdin-agent"; + }; +}) diff --git a/pkgs/by-name/qe/qemu/package.nix b/pkgs/by-name/qe/qemu/package.nix index 31515f58d636..dea71a96bd84 100644 --- a/pkgs/by-name/qe/qemu/package.nix +++ b/pkgs/by-name/qe/qemu/package.nix @@ -140,11 +140,11 @@ stdenv.mkDerivation (finalAttrs: { + lib.optionalString nixosTestRunner "-for-vm-tests" + lib.optionalString toolsOnly "-utils" + lib.optionalString userOnly "-user"; - version = "10.2.2"; + version = "11.0.0"; src = fetchurl { url = "https://download.qemu.org/qemu-${finalAttrs.version}.tar.xz"; - hash = "sha256-eEspb/KcFBeqcjI6vLLS6pq5dxck9Xfc14XDsE8h4XY="; + hash = "sha256-wEyjYBJlPzLRHGdNNwz1KnEOfT8Ywti2PkkyBSpIVNY="; }; depsBuildBuild = [ @@ -165,6 +165,8 @@ stdenv.mkDerivation (finalAttrs: { # For python changes other than simple package additions, ping @dramforever for review. # Don't change `python3Packages` to `python3.pkgs.*`, breaks cross-compilation. python3Packages.distlib + python3Packages.setuptools + python3Packages.wheel # Hooks from the python package are needed to add `$pythonPath` so # `python/scripts/mkvenv.py` can detect `meson` otherwise the vendored meson without patches will be used. python3Packages.python @@ -374,7 +376,11 @@ stdenv.mkDerivation (finalAttrs: { # tests can still timeout on slower systems doCheck = false; - nativeCheckInputs = [ socat ]; + nativeCheckInputs = [ + python3Packages.pygdbmi + python3Packages.qemu-qmp + socat + ]; preCheck = '' # time limits are a little meagre for a build machine that's # potentially under load. @@ -444,7 +450,7 @@ stdenv.mkDerivation (finalAttrs: { license = lib.licenses.gpl2Plus; maintainers = with lib.maintainers; [ qyliss ]; teams = lib.optionals xenSupport xen.meta.teams; - platforms = lib.platforms.unix; + platforms = with lib.systems.inspect; patternLogicalAnd patterns.is64bit patterns.isUnix; } # toolsOnly: Does not have qemu-kvm and there's no main support tool # userOnly: There's one qemu- for every architecture @@ -453,7 +459,14 @@ stdenv.mkDerivation (finalAttrs: { } # userOnly: https://qemu.readthedocs.io/en/master/user/main.html#supported-operating-systems // lib.optionalAttrs userOnly { - platforms = with lib.platforms; (linux ++ freebsd ++ openbsd ++ netbsd); + platforms = + with lib.systems.inspect; + patternLogicalAnd patterns.is64bit [ + patterns.isLinux + patterns.isFreeBSD + patterns.isOpenBSD + patterns.isNetBSD + ]; description = "QEMU User space emulator - launch executables compiled for one CPU on another CPU"; }; }) diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index 08d40e4e30ff..75992b42240e 100644 --- a/pkgs/by-name/ru/ruff/package.nix +++ b/pkgs/by-name/ru/ruff/package.nix @@ -16,7 +16,7 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ruff"; - version = "0.15.14"; + version = "0.15.15"; __structuredAttrs = true; @@ -24,12 +24,12 @@ rustPlatform.buildRustPackage (finalAttrs: { owner = "astral-sh"; repo = "ruff"; tag = finalAttrs.version; - hash = "sha256-Z8UhVS+YbYAxVWodU/I+p3Ns5/EpmzBTChcbkvJwe6Y="; + hash = "sha256-WpjOOCYLZ1d8XPUx3qNHD+fuK6t65u/1/ZezABWpBD0="; }; cargoBuildFlags = [ "--package=ruff" ]; - cargoHash = "sha256-GnRC5jXySAna7uAKPDtpPQUJe8AKqVSU+ynmGKZtfTs="; + cargoHash = "sha256-SfkoLl43Y1DNqIRW+HljVcEHWhedTS99SGhMvkQ4dmo="; nativeBuildInputs = [ installShellFiles ]; diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs index d9568c7b5464..20c80a50baa5 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/main.rs @@ -688,12 +688,7 @@ fn handle_modified_unit( let reload_list = scope.reload_list_file(); let use_restart_as_stop_and_start = new_unit_info.is_none(); - if matches!( - unit, - "sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target" - ) || unit.ends_with(".unit") - || unit.ends_with(".slice") - { + if cannot_be_restarted_directly(unit) { // Do nothing. These cannot be restarted directly. // Slices and Paths don't have to be restarted since properties (resource limits and @@ -940,6 +935,17 @@ fn parse_fstab(fstab: impl BufRead) -> (HashMap, HashMap bool { + matches!( + unit, + "sysinit.target" | "basic.target" | "multi-user.target" | "graphical.target" + ) || unit.ends_with(".path") + || unit.ends_with(".slice") +} + // Returns a HashMap containing the same contents as the passed in `units`, minus the units in // `units_to_filter`. fn filter_units( @@ -957,6 +963,50 @@ fn filter_units( res } +/// Action to take on a unit that migrated to NixOS ownership during the +/// post-activation pass. Honours the same X-* directives as +/// `handle_modified_unit`. +#[derive(Debug, PartialEq)] +enum MigrationAction { + Skip, + Reload, + Restart, + Start, +} + +impl MigrationAction { + /// Action to take on a migrated unit that is still active. + fn for_active_unit(unit: &str, new_unit_info: &UnitInfo) -> Self { + if cannot_be_restarted_directly(unit) { + return Self::Skip; + } + + if parse_systemd_bool(Some(new_unit_info), "Service", "X-ReloadIfChanged", false) { + return Self::Reload; + } + + if !parse_systemd_bool(Some(new_unit_info), "Service", "X-RestartIfChanged", true) + || parse_systemd_bool(Some(new_unit_info), "Unit", "RefuseManualStop", false) + || parse_systemd_bool(Some(new_unit_info), "Unit", "X-OnlyManualStart", false) + { + return Self::Skip; + } + + Self::Restart + } + + /// Action to take on a migrated unit that the previous owner stopped. + fn for_stopped_unit(new_unit_info: &UnitInfo) -> Self { + if parse_systemd_bool(Some(new_unit_info), "Unit", "RefuseManualStart", false) + || parse_systemd_bool(Some(new_unit_info), "Unit", "X-OnlyManualStart", false) + { + return Self::Skip; + } + + Self::Start + } +} + fn unit_is_active(conn: &LocalConnection, unit: &str) -> Result { let unit_object_path = conn .with_proxy( @@ -1345,26 +1395,79 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { let current_active_units = get_active_units(&systemd)?; + let old_unit_dir = old_toplevel.join(scope.etc_dir()); let new_unit_dir = toplevel.join(scope.etc_dir()); - let fragment_prefix = scope - .current_dir() - .to_str() - .expect("scope dir is valid UTF-8"); + let fragment_dir = scope.current_dir(); - // Units that are currently running from a non-/etc location (typically - // ~/.config/systemd/user, i.e. home-manager) but that the new NixOS - // configuration also defines. Pass 1 will skip these because of the - // FragmentPath filter; if the per-user activation (sd-switch) later drops - // its copy, we need a second pass to bring the NixOS-owned definition up. + // Determine $XDG_CONFIG_HOME/systemd/user from the user manager's own + // environment (we are spawned with env_clear()). + let user_config_unit_dir: Option = match systemd.environment() { + Err(err) => { + log::debug!("Failed to read user manager environment: {err}"); + None + } + Ok(env) => { + let lookup = |key: &str| { + env.iter().find_map(|kv| { + kv.strip_prefix(key) + .and_then(|rest| rest.strip_prefix('=')) + .filter(|v| Path::new(v).is_absolute()) + .map(PathBuf::from) + }) + }; + let config_home = + lookup("XDG_CONFIG_HOME").or_else(|| lookup("HOME").map(|h| h.join(".config"))); + if config_home.is_none() { + log::debug!( + "Neither $XDG_CONFIG_HOME nor $HOME is set in the user manager's environment" + ); + } + config_home.map(|config_home| config_home.join("systemd/user")) + } + }; + + if user_config_unit_dir.is_none() { + log::debug!( + "Could not determine $XDG_CONFIG_HOME/systemd/user; \ + units shadowed by ~/.config will not be considered for migration" + ); + } + + // Units active from a non-/etc location that the new generation defines + // in /etc/systemd/user. Pass 1 skips these (FragmentPath filter); pass 2 + // brings the /etc definition into effect once /etc has won. Two cases: + // * ~/.config/systemd/user (home-manager): shadows /etc, so wait for + // the per-user activation (sd-switch) to remove its copy. + // * anywhere else outside /etc ($XDG_DATA_HOME, $XDG_DATA_DIRS, ...): + // /etc outranks these, so only act when /etc is gaining the unit; + // if the previous generation already had it, leave it alone. + // Pass 2's `now_etc` check verifies /etc actually won before acting. let migration_candidates: Vec = current_active_units .iter() .filter(|(unit, _)| new_unit_dir.join(unit).exists()) - .filter(|(_, unit_state)| { - !unit_state + .filter(|(unit, unit_state)| { + let Ok(fragment_path) = unit_state .proxy - .get("org.freedesktop.systemd1.Unit", "FragmentPath") - .map(|p: String| p.starts_with(fragment_prefix)) - .unwrap_or(false) + .get::("org.freedesktop.systemd1.Unit", "FragmentPath") + else { + return false; + }; + let fragment_parent = Path::new(&fragment_path).parent(); + + // Already in /etc: handled by pass 1. + if fragment_parent == Some(fragment_dir) { + return false; + } + + // Loaded from ~/.config/systemd/user, which shadows /etc. + if let Some(dir) = &user_config_unit_dir { + if fragment_parent == Some(dir.as_path()) { + return true; + } + } + + // Elsewhere: only act if /etc is gaining the unit this switch. + !old_unit_dir.join(unit).exists() }) .map(|(unit, _)| unit.clone()) .collect(); @@ -1372,7 +1475,7 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { collect_unit_changes( &toplevel, scope, - &old_toplevel.join(scope.etc_dir()), + &old_unit_dir, &new_unit_dir, ¤t_active_units, &mut units_to_stop, @@ -1383,6 +1486,9 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { &mut units_to_filter, )?; + // Restarted unconditionally below; don't list it as skipped. + units_to_skip.remove("nixos-activation.service"); + let print_units = |verb: &str, units: &HashMap| { if units.is_empty() { return; @@ -1483,6 +1589,7 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { // Toplevels with system.activatable = false do not ship this unit; mirror // the system scope's tolerance for a missing activate script. if new_unit_dir.join("nixos-activation.service").exists() { + eprintln!("restarting the following user units: nixos-activation.service"); match systemd.restart_unit("nixos-activation.service", "replace") { Ok(_) => { log::debug!("waiting for nixos activation to finish"); @@ -1510,29 +1617,40 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { let active_after = get_active_units(&systemd)?; + let mut to_reload = HashMap::new(); let mut to_restart = HashMap::new(); let mut to_start = HashMap::new(); + let mut to_skip = HashMap::new(); for unit in &migration_candidates { - match active_after.get(unit) { + // Honour X-* directives so reloadIfChanged/restartIfChanged hold. + let new_unit_file = new_unit_dir.join(unit); + let new_unit_info = parse_unit(&new_unit_file, &new_unit_file)?; + + let action = match active_after.get(unit) { Some(unit_state) => { + // Only act if /etc now wins (i.e. the higher-priority + // copy is gone). Read errors are treated as "leave alone". let now_etc = unit_state .proxy .get("org.freedesktop.systemd1.Unit", "FragmentPath") - .map(|p: String| p.starts_with(fragment_prefix)) + .map(|p: String| Path::new(&p).parent() == Some(fragment_dir)) .unwrap_or(false); - if now_etc { - // Still running with the previous manager's binary; - // restart so the /etc definition takes effect. - to_restart.insert(unit.clone(), ()); + if !now_etc { + // Still shadowed (or read error); leave it alone. + continue; } - // else: still shadowed by ~/.config, leave it alone. + MigrationAction::for_active_unit(unit, &new_unit_info) } - None => { - // Stopped by the previous manager; start the /etc copy. - to_start.insert(unit.clone(), ()); - } - } + // Stopped by the previous manager; start the /etc copy. + None => MigrationAction::for_stopped_unit(&new_unit_info), + }; + match action { + MigrationAction::Skip => to_skip.insert(unit.clone(), ()), + MigrationAction::Reload => to_reload.insert(unit.clone(), ()), + MigrationAction::Restart => to_restart.insert(unit.clone(), ()), + MigrationAction::Start => to_start.insert(unit.clone(), ()), + }; } // Re-start active targets so any other newly-unmasked dependencies are @@ -1543,6 +1661,24 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { } } + if !to_skip.is_empty() { + print_units("NOT restarting (post-activation)", &to_skip); + } + + print_units("reloading (post-activation)", &to_reload); + for unit in to_reload.keys() { + match systemd.reload_unit(unit, "replace") { + Ok(job_path) => { + submitted_jobs.borrow_mut().insert(job_path, Job::Reload); + } + Err(err) => { + eprintln!("Failed to reload user unit {unit}: {err}"); + exit_code = 4; + } + } + } + block_on_jobs(&dbus_conn, &submitted_jobs); + print_units("restarting (post-activation)", &to_restart); for unit in to_restart.keys() { match systemd.restart_unit(unit, "replace") { @@ -2863,4 +2999,107 @@ After=dev-disk-by\x2dlabel-root.device ); } } + + fn unit_info( + sections: &[(&str, &[(&str, &str)])], + ) -> HashMap>> { + sections + .iter() + .map(|(section, kvs)| { + ( + section.to_string(), + kvs.iter() + .map(|(k, v)| (k.to_string(), vec![v.to_string()])) + .collect(), + ) + }) + .collect() + } + + #[test] + fn migration_action_for_active_unit() { + use super::MigrationAction; + + // Plain service: restart. + assert_eq!( + MigrationAction::for_active_unit("foo.service", &unit_info(&[])), + MigrationAction::Restart + ); + + // reloadIfChanged must reload, not restart. + assert_eq!( + MigrationAction::for_active_unit( + "foo.service", + &unit_info(&[("Service", &[("X-ReloadIfChanged", "true")])]) + ), + MigrationAction::Reload + ); + + // X-RestartIfChanged=false (restartIfChanged = false) must skip. + assert_eq!( + MigrationAction::for_active_unit( + "foo.service", + &unit_info(&[("Service", &[("X-RestartIfChanged", "false")])]) + ), + MigrationAction::Skip + ); + + // RefuseManualStop must skip. + assert_eq!( + MigrationAction::for_active_unit( + "foo.service", + &unit_info(&[("Unit", &[("RefuseManualStop", "yes")])]) + ), + MigrationAction::Skip + ); + + // X-OnlyManualStart must skip. + assert_eq!( + MigrationAction::for_active_unit( + "foo.service", + &unit_info(&[("Unit", &[("X-OnlyManualStart", "yes")])]) + ), + MigrationAction::Skip + ); + + // Units that cannot be restarted directly must skip. + for unit in [ + "sysinit.target", + "basic.target", + "multi-user.target", + "graphical.target", + "foo.path", + "bar.slice", + ] { + assert_eq!( + MigrationAction::for_active_unit(unit, &unit_info(&[])), + MigrationAction::Skip, + "{unit}" + ); + } + } + + #[test] + fn migration_action_for_stopped_unit() { + use super::MigrationAction; + + assert_eq!( + MigrationAction::for_stopped_unit(&unit_info(&[])), + MigrationAction::Start + ); + assert_eq!( + MigrationAction::for_stopped_unit(&unit_info(&[( + "Unit", + &[("RefuseManualStart", "true")] + )])), + MigrationAction::Skip + ); + assert_eq!( + MigrationAction::for_stopped_unit(&unit_info(&[( + "Unit", + &[("X-OnlyManualStart", "true")] + )])), + MigrationAction::Skip + ); + } } diff --git a/pkgs/by-name/ty/ty/package.nix b/pkgs/by-name/ty/ty/package.nix index 0dde0db10465..8b1c65bf1be6 100644 --- a/pkgs/by-name/ty/ty/package.nix +++ b/pkgs/by-name/ty/ty/package.nix @@ -17,7 +17,7 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ty"; - version = "0.0.38"; + version = "0.0.40"; __structuredAttrs = true; src = fetchFromGitHub { @@ -25,7 +25,7 @@ rustPlatform.buildRustPackage (finalAttrs: { repo = "ty"; tag = finalAttrs.version; fetchSubmodules = true; - hash = "sha256-70Y5i9m2h2+Jc44jLOf+gXX/PeDbURRJ80y+6h5SlRk="; + hash = "sha256-kdfPnyQXYtf3BDrYCFGfX0bMoPGjRpyH3aUeRZBiUKY="; }; # For Darwin platforms, remove the integration test for file notifications, @@ -39,7 +39,7 @@ rustPlatform.buildRustPackage (finalAttrs: { cargoBuildFlags = [ "--package=ty" ]; - cargoHash = "sha256-+c2JfB55w9otmmgTFIDMwkpASJV7bIMEf0uqRXjk/QM="; + cargoHash = "sha256-yUbHTzUGNdpm3b1s/SkcpFGdp7WjN+xO+CVrPPwrh6A="; nativeBuildInputs = [ installShellFiles ]; buildInputs = [ rust-jemalloc-sys ]; @@ -101,6 +101,7 @@ rustPlatform.buildRustPackage (finalAttrs: { mainProgram = "ty"; maintainers = with lib.maintainers; [ bengsparks + ddogfoodd figsoda GaetanLepage ]; diff --git a/pkgs/os-specific/linux/kernel/build.nix b/pkgs/os-specific/linux/kernel/build.nix index ac191a3ba149..23a08f5beaa4 100644 --- a/pkgs/os-specific/linux/kernel/build.nix +++ b/pkgs/os-specific/linux/kernel/build.nix @@ -19,7 +19,6 @@ zlib, pahole, kmod, - ubootTools, fetchpatch, rustc-unwrapped, rust-bindgen-unwrapped, @@ -116,31 +115,6 @@ lib.makeOverridable ( ; }; - # Folding in `ubootTools` in the default nativeBuildInputs is problematic, as - # it makes updating U-Boot cumbersome, since it will go above the current - # threshold of rebuilds - # - # To prevent these needless rounds of staging for U-Boot builds, we can - # limit the inclusion of ubootTools to target platforms where uImage *may* - # be produced. - # - # This command lists those (kernel-named) platforms: - # .../linux $ grep -l uImage ./arch/*/Makefile | cut -d'/' -f3 | sort - # - # This is still a guesstimation, but since none of our cached platforms - # coincide in that list, this gives us "perfect" decoupling here. - linuxPlatformsUsingUImage = [ - "arc" - "arm" - "csky" - "mips" - "powerpc" - "sh" - "sparc" - "xtensa" - ]; - needsUbootTools = lib.elem stdenv.hostPlatform.linuxArch linuxPlatformsUsingUImage; - config = let attrName = attr: "CONFIG_" + attr; @@ -267,7 +241,6 @@ lib.makeOverridable ( kmod hexdump ] - ++ optional needsUbootTools ubootTools ++ optionals (lib.versionAtLeast version "5.2") [ cpio pahole @@ -510,7 +483,7 @@ lib.makeOverridable ( preFixup = '' if [ -z "''${dontStrip-}" -a -e $out/vmlinux ]; then - strip -v -S -p $out/vmlinux + $STRIP -v -S -p $out/vmlinux fi ''; @@ -536,12 +509,10 @@ lib.makeOverridable ( kernelAtLeast = lib.versionAtLeast baseVersion; }; - # Some image types need special install targets (e.g. uImage is installed with make uinstall on arm) + # Some image types need special install targets installTargets = [ (stdenv.hostPlatform.linux-kernel.installTarget or ( - if target == "uImage" && stdenv.hostPlatform.linuxArch == "arm" then - "uinstall" - else if + if (target == "zImage" || target == "Image.gz" || target == "vmlinuz.efi") && builtins.elem stdenv.hostPlatform.linuxArch [ "arm" diff --git a/pkgs/os-specific/linux/kernel/common-config.nix b/pkgs/os-specific/linux/kernel/common-config.nix index d1734a2b32c5..e0083addb03f 100644 --- a/pkgs/os-specific/linux/kernel/common-config.nix +++ b/pkgs/os-specific/linux/kernel/common-config.nix @@ -773,6 +773,9 @@ let SQUASHFS_LZ4 = yes; SQUASHFS_ZSTD = yes; + EROFS_FS_ZIP_DEFLATE = whenAtLeast "6.6" yes; + EROFS_FS_ZIP_ZSTD = whenAtLeast "6.10" yes; + # Native Language Support modules, needed by some filesystems NLS = yes; NLS_DEFAULT = freeform "utf8"; diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index fd9cf5dac790..f2e87f79b99c 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -1,42 +1,42 @@ { "testing": { - "version": "7.1-rc4", - "hash": "sha256:06z73f1vprl4adbdj01h3407p3f8bl8jsz91zx454c62k2jg3w40", + "version": "7.1-rc6", + "hash": "sha256:1fmbsjhdrkzim6vzqc40raikv1szfw28q0lbvap8a1g77an0qi58", "lts": false }, "6.1": { - "version": "6.1.174", - "hash": "sha256:0vp07x4v82qnmc1pifv3ynp2ab5mvlbfnpqvs5893bi3yrnk927d", + "version": "6.1.175", + "hash": "sha256:11fapr04y96p9ja6mfzm7bcd3zb4dzyw6qrh7c11bss9wjlq9s9p", "lts": true }, "5.15": { - "version": "5.15.208", - "hash": "sha256:0wmi50q8vgblhbh77d1a4sw4snymr6srqd22bxcjg9i7wcv70gdm", + "version": "5.15.209", + "hash": "sha256:1d0yhbpqlkr1znahky15dfavr6dzb3wb8c15k9qqvkf2xb3pfv9l", "lts": true }, "5.10": { - "version": "5.10.257", - "hash": "sha256:1lghcrxc1fqarvym03jrcda2a3labc887ci9yjqgbmv3nphzvc88", + "version": "5.10.258", + "hash": "sha256:1rdldzb3g33v6zvcmxafqpkjgqpp4n5qlxwb77wfd5jpzhgcnz4y", "lts": true }, "6.6": { - "version": "6.6.141", - "hash": "sha256:1qbzxgqs7q9gyqfrf0j7p0pgjxnjj5mibamhm280mf9anqp6bhiv", + "version": "6.6.142", + "hash": "sha256:0w1bdzp9x1sqcr9xlk7dvylhs7kycghjabfgd3iv49ydfmx61xmj", "lts": true }, "6.12": { - "version": "6.12.91", - "hash": "sha256:0sbrb612b653w64g5jkpbf68y0fka2sgnwblam41k7wz2sgapwhg", + "version": "6.12.92", + "hash": "sha256:0gly5wld3x8l0f3zk9pspsw1q2d7zbjbx4c2ndb49b1wvfvpdqqg", "lts": true }, "6.18": { - "version": "6.18.33", - "hash": "sha256:10mp1ypsdz42jr26g1xxbw806mvpy0n35418fhsgxxlr4lqgy5kg", + "version": "6.18.34", + "hash": "sha256:0q6palsvwx0gnisjr658hlngfpvyzv0k5q4pvdk23122zcr4f334", "lts": true }, "7.0": { - "version": "7.0.10", - "hash": "sha256:1p1j9s0b4qv9m0pm6vj477rqgyd1b0lsk0gy74cks3n2cbmpfj89", + "version": "7.0.11", + "hash": "sha256:012307ni1v555a1rgzsxsg99pj8fplrghvhw0jk3c4d0vmb86v75", "lts": false } } diff --git a/pkgs/stdenv/linux/make-bootstrap-tools-cross.nix b/pkgs/stdenv/linux/make-bootstrap-tools-cross.nix index d69cc836a19e..5b670400ef43 100644 --- a/pkgs/stdenv/linux/make-bootstrap-tools-cross.nix +++ b/pkgs/stdenv/linux/make-bootstrap-tools-cross.nix @@ -25,7 +25,7 @@ lib.mapAttrs (n: make) ( # on how to request an upload. # Sort following the sorting in `./default.nix` `bootstrapFiles` argument. - armv5tel-unknown-linux-gnueabi = sheevaplug; + armv5tel-unknown-linux-gnueabi = armv5tel-multiplatform; armv6l-unknown-linux-gnueabihf = raspberryPi; armv7l-unknown-linux-gnueabihf = armv7l-hf-multiplatform; aarch64-unknown-linux-gnu = aarch64-multiplatform; diff --git a/pkgs/test/cross/default.nix b/pkgs/test/cross/default.nix index c5320f72a4bc..b4a007fa17e2 100644 --- a/pkgs/test/cross/default.nix +++ b/pkgs/test/cross/default.nix @@ -189,7 +189,7 @@ let pkgs.pkgsLLVM.stdenv pkgs.pkgsStatic.bash pkgs.pkgsCross.arm-embedded.stdenv - pkgs.pkgsCross.sheevaplug.stdenv # for armv5tel + pkgs.pkgsCross.armv5tel-multiplatform.stdenv pkgs.pkgsCross.raspberryPi.stdenv # for armv6l pkgs.pkgsCross.armv7l-hf-multiplatform.stdenv pkgs.pkgsCross.m68k.stdenv diff --git a/pkgs/top-level/release-cross.nix b/pkgs/top-level/release-cross.nix index e264d1cf8590..cc0ab49a8c40 100644 --- a/pkgs/top-level/release-cross.nix +++ b/pkgs/top-level/release-cross.nix @@ -197,8 +197,8 @@ in crossIphone32 = mapTestOnCross systems.examples.iphone32 darwinCommon; - # Test some cross builds to the Sheevaplug - crossSheevaplugLinux = mapTestOnCross systems.examples.sheevaplug ( + # Test some cross builds to ARMv5 + armv5tel = mapTestOnCross systems.examples.armv5tel-multiplatform ( linuxCommon // { ubootSheevaplug = nativePlatforms; @@ -235,8 +235,6 @@ in # Linux on armv7l-hf armv7l-hf = mapTestOnCross systems.examples.armv7l-hf-multiplatform linuxCommon; - pogoplug4 = mapTestOnCross systems.examples.pogoplug4 linuxCommon; - # Linux on aarch64 aarch64 = mapTestOnCross systems.examples.aarch64-multiplatform linuxCommon; aarch64-musl = mapTestOnCross systems.examples.aarch64-multiplatform-musl linuxCommon;