NixOS custom directory with uid/gid/mode? (And, bind mount?)

I want my NixOS configuration to do the following upon running nixos-rebuild switch: create a directory (anywhere—really) with a custom uid/gid/mode specified, and with a single, empty file inside it (which I’ll later use to bind mount to.)

I thought environment.etc.<name>.* sounded like a good solution, but, I can’t figure out how to do what I described above.

What’s the NixOS way to do this?

(The reason “why” is to support libvirt+JACK+PipeWire.)

UPDATE: This was an X-Y problem. The solution was to set services.pipewire.systemWide = true. (And to work around a current bug, to also set wireplumber.enable = true, and media-session.enable = false.)

environment.etc can’t directly create files, it just creates links into the store.

tmpfiled can manage bind mounts IIRC or create empty files or copy others.

Thanks, systemd.tmpfiles.rules looks interesting.

Maybe I can create this structure with tmpfiles. But when do I do the bind mount? What I’m bind-mounting from is PipeWire’s /run/user/1000/pipewire-0 socket, which gets re-initialized after each boot (probably inside the pipewire service?) Do I need to do some kind of post-service hook? * Head scratch *

For the bind mount, something like the following:

fileSystems."/srv/nfs" = {
  device = "/data";
  options = [ "bind" ];

And, as you already pointed out, systemdtmpfiles.rules is your best bet for creating the file with desired permissions.

1 Like

How would I create a tmpfile with uid/gid and content?

I made progress but still haven’t solved this. I could still use help.

I’m successfully able to create the temp files I need:

systemd.tmpfiles.rules = [
    "d /run/pipewire-shared            0550 1000 qemu-libvirtd"           
    "f /run/pipewire-shared/pipewire-0 0555 root root"        

I’m also successfully able to configure the bind mount I need in /etc/fstab by doing this:

fileSystems."/run/pipewire-shared/pipewire-0" = {
    device = "/run/user/1000/pipewire-0";
    options = [ "bind" "noauto" "user" "rw" ];
    fsType = "none";

The final, unsolved, step is to actually perform the mount. I can do this manually by running:

mount /run/pipewire-shared/pipewire-0

Great! But, I don’t want to have to run this manually. Also, I don’t think I can simply configure the mount as an auto-mount, because the source file which I’m bind-mounting from, /run/user/1000/pipewire-0, first must be available, and it’s initialized in a user-mode systemd socket: pipewire.socket.

So, I need the command mount /run/pipewire-shared/pipewire-0 to be run, immediately time after the systemd pipewire.socket is initialized. I tried the following:"pipewire-shared-mount" = {
  after = [ "pipewire.socket" ];
  script = ''
  exec 2>&1
  ${pkgs.mount}/bin/mount /run/pipewire-shared/pipewire-0

The service is created, but its script exits with exit code 32, and the error message: “mount: /run/pipewire-shared/pipewire-0: must be superuser to use mount”. Interestingly, this command works fine when I run it manually.

Here are the systemd logs:

$ journalctl --user-unit pipewire-shared-mount
Jun 01 00:20:09 nixos systemd[1784]: Started pipewire-shared-mount.service.
Jun 01 10:28:23 nixos pipewire-shared-mount-start[197439]: mount: /run/pipewire-shared/pipewire-0: must be superuser to use mount.
Jun 01 10:28:23 nixos systemd[1784]: pipewire-shared-mount.service: Main process exited, code=exited, status=32/n/a
Jun 01 10:28:23 nixos systemd[1784]: pipewire-shared-mount.service: Failed with result 'exit-code'.

Not to be abused but something like this:

  systemd.tmpfiles.rules = [
    "d /var/www/"
    "f /var/www/ - root root - <?php phpinfo();"

You don’t know by chance how to add multiline text like this?

Yes, though you should really use this sparingly…

systemd.tmpfiles.rules =
    text = builtins.replaceStrings [ "\n" "\"" "\\" ] [ "\\n" "\\\"" "\\\\" ] ''
      here is a line of "text"
      and another line of "text"
      // you need to figure out what additional escapes you need and add them above
    [ "f /foo.txt - root root ${text}" ];
1 Like

I found out that the systemd service is failing to mount, with the error:

mount: /run/pipewire-shared/pipewire-0: must be superuser to use mount.

But, the command works when I run it on the command line.

I made progress, but am now dealing with a new, NixOS specific issue.

Here’s what is (mostly) working:

systemd.tmpfiles.rules = [
  "d /run/pipewire-shared            0777 1000 qemu-libvirtd"
  "f /run/pipewire-shared/pipewire-0 0777 root root"

fileSystems."/run/pipewire-shared/pipewire-0" = {
  device = "/run/user/1000/pipewire-0";
  options = [ "bind" "noauto" "user" "rw" ];
  fsType = "none";
};"pipewire-shared-mount" = {
  after = [ "pipewire.socket" ];
  requires = [ "pipewire.socket" ];
  wantedBy = [ "" ];
  serviceConfig = {
    ExecStart = "/run/wrappers/bin/mount /run/pipewire-shared/pipewire-0";
    ExecStop = "/run/wrappers/bin/umount /run/pipewire-shared/pipewire-0";
    RemainAfterExit = "yes";

After this, when I start/stop the user service (pipewire-shared-mount), it successfully bind mounts/unmounts /run/pipewire-shared/pipewire-0. Great!


When the I run nixos-rebuild switch, it fails to rebuild the filesystem, because the bind mount is snagging it:

/run/pipewire-shared/pipewire-0 exists and is not a regular file.

Hmm, how can I have my service stop/stop automatically whenever NixOS is rebuilding? Maybe there’s a NixOS systemd user-service I can make my service depend on?

have you noticed the fileSystems.<name>.depends option? This basically allows you to set a path dependency so a mount is not intiated until the specified path exists first. Probably a nicer solution that trying to hack it together manually.

Great suggestion, but doesn’t work :frowning: .

What this does, according to the docs, “If a path is added to this list, any other filesystem whose mount point is a parent of the path will be mounted before this filesystem”.

This makes it sound like my only causal dependencies are fstab mounts. But, that’s not true: there’s also a systemd service (pipewire.socket) which I need to wait for, first.

Is there any way I can “hook” into nixos-rebuild to stop my service before it runs?

Here is my take on it from slightly different angle:

  1. Running arbitrary command at rebuild or bootup could be done with system.activationScripts. I’m using it toset arbitrary file permissions:
  system.activationScripts.foo_home_read = pkgs.lib.stringAfter [ "users" ]
      # allow all users peek at the configs
      chmod g+rx /home/foo
  1. Exporting pipewire for other users could be done by creating extra socket with more accessible path. I’m using the following:
  services.pipewire.pulse.enable = true;
  # allow other user use sound by absolute address:
  services.pipewire.config.pipewire-pulse = {
    "" = {
      "server.address" = [
        # default:
        # extension:
  hardware.pulseaudio.extraClientConf = ''
# cat ~/.config/systemd/user/pipewire-pulse.socket.d/override.conf

I tried setting permissions just like you show here in systems.activationScripts but for some reason they never took affect, which is why I started using tmpfiles.d for this purpose. Maybe it has something to do with the fact that the directory I was trying to change perms on was also a fileSystems mount :man_shrugging:

1 Like

Your solution isn’t working on my machine:

$ ls  ~/.config/systemd/user redshift.service

I’m using nixos-22.05, and my other pipewire settings are:

security.rtkit.enable = true;
services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    jack.enable = true;
    pulse.enable = true;
    socketActivation = true;
    wireplumber.enable = false;
    media-session.enable = true;

Regardless, isn’t this PulseAudio? I really need PipeWire’s JACK emulation layer. Does your code snippet result in sharing the /run/user/1000/pulse/native (which I believe only works for PulseAudio emulation), or the the /run/user/1000/pipewire-0?

(I’ll add that I’m in over my head here, definitely not an expert by any means with NixOS or PipeWire.)

Doh! I just found this setting:




If true, a system-wide PipeWire service and socket is enabled allowing all users in the “pipewire” group to use it simultaneously. If false, then user units are used instead, restricting access to only one user. Enabling system-wide PipeWire is however not recommended and disabled by default according to pipewire/NEWS at 0d51f3b74e1efc2787e29f00e0ccc4b28b5da8a6 · PipeWire/pipewire · GitHub

This does exactly what I was trying to do!

It looks like it puts the socket at /run/pipewire/pipewire-0.

NOTE: I noticed sound only works if using media-session, and not wireplumber. See this comment.

Tl;DR: please ignore my example with multiple sockets above. It seems to be feasible only for pulse protocol. The rest is clarification of my pipewire-pulseaudio sharing setup.

I did set it up only for pulseaudio as it’s the only protocol I needed to share across users (mostly for wine).

I had to create ~/.config/systemd/user/pipewire-pulse.socket.d/override.conf file manually in primary user’s directory to allow socket activation by anyone who tries to touch newly added server.address socket name.

I hoped that the similar approach would work if we were to try to share pipewire’s native socket. But it looks like pipewire-0 location is hardcoded for pipewire protocol.