Nimbin[12]?Linux From Scratch / Packagemanager-LFS-PackageUser-System / commits / adfa88c

Packagemanager-LFS-PackageUser-System git · main

Package-user based package manager for LFS/BLFS

lfs blfs package-manager bash · first commit 2026-08-18 · last commit 2026-10-26 (8 days ago) · synced 8 hours ago · upstream: github.com/nimbin2/Packagemanager-LFS-PackageUser-System

Shell 63.7% Markdown 35.8%
git clone https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System.gitwget https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System/archive/Packagemanager-LFS-PackageUser-System.tar.gz

update

Christian Immanuel · 2026-08-24 03:35 · adfa88c94946bf523f5e70c4d55d26c05743df10

 .bash_profile  |  42 +++++++++++
 Makefile       |  23 ++++--
 README.md      |  19 ++++-
 packagemanager | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 298 insertions(+), 20 deletions(-)

diff --git a/.bash_profile b/.bash_profile
new file mode 100644
index 0000000..8f78d9d
--- /dev/null
+++ b/.bash_profile
@@ -0,0 +1,42 @@
+# /etc/pkgusr/skel-u_xdg/.bash_profile
+#
+# The login profile for SHARED package users -- the ones in the group on your
+# XDG runtime directory.  It is what lets a program running as its own account
+# reach your display, session bus and audio.
+#
+# Linked into a shared user's home by:
+#     packagemanager user create <name> --shared
+#     packagemanager user setup  <name> --shared
+#
+# XDG_RUNTIME_DIR below is written when the file is first installed, from the
+# configured main user.  Edit it if your session lives somewhere else.
+
+# The wrappers directory must be the first entry in the PATH.
+# The /tools/bin directory must be the last entry in the PATH and can be
+#   removed at the end of Chapter 6.
+export PATH=/usr/lib/pkgusr:/bin:/usr/bin:/sbin:/usr/sbin:/tools/bin
+
+# Make prompt reflect that we are a package user.
+export PROMPT_COMMAND='PS1="package \u:"`pwd`"> "'
+
+# The session this account is allowed to reach.
+export WAYLAND_DISPLAY=wayland-1
+#export DISPLAY=:0
+#export DBUS_SESSION_BUS_ADDRESS=/dev/null # fake dbus so we dont start one...
+export XDG_RUNTIME_DIR=@XDG_RUNTIME_DIR@
+
+# Go to the home directory whenever we su to a package user.
+cd
+
+if [ -d "$HOME/bin" ] ; then
+  pathprepend $HOME/bin
+fi
+if [ -d "$HOME/.local/bin" ] ; then
+  pathprepend $HOME/.local/bin
+fi
+
+for script in /etc/profile.d/*.sh ; do
+    if [ -r $script ] ; then
+        . $script
+    fi
+done
diff --git a/Makefile b/Makefile
index 52e27a3..d37b440 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 # lfs-pkgusr -- install the package-user toolchain.
 #
-#   make install                 -> /usr/local
-#   make install PREFIX=/usr     -> /usr
+#   make install                    -> /usr
+#   make install PREFIX=/usr/local  -> /usr/local
 #   make uninstall
 #   make check                   -> run the regression tests
 #
@@ -9,7 +9,10 @@
 # into a tree that is not the running system:
 #   make install DESTDIR=/mnt/lfs PREFIX=/usr
 
-PREFIX      ?= /usr/local
+# /usr, not /usr/local: these tools ARE the system's package management -- the
+# chroot invokes them by name, and packagemanager runs packagemanager_install
+# off $PATH, which does not include /usr/local everywhere.
+PREFIX      ?= /usr
 DESTDIR     ?=
 BINDIR      := $(DESTDIR)$(PREFIX)/bin
 SHAREDIR    := $(DESTDIR)$(PREFIX)/share/lfs-pkgusr
@@ -29,8 +32,8 @@ INSTALL     ?= install
 all: help
 
 help:
-	@echo "make install [PREFIX=/usr] [DESTDIR=...]   install the tools"
-	@echo "make uninstall [PREFIX=/usr]               remove them"
+	@echo "make install [PREFIX=...] [DESTDIR=...]   install the tools"
+	@echo "make uninstall [PREFIX=...]               remove them"
 	@echo "make check                                 run the tests"
 	@echo ""
 	@echo "installs into: $(PREFIX)/bin"
@@ -51,6 +54,15 @@ install:
 	@for t in blfs packagemanager lfs-helper; do \
 	    ln -sf lfs "$(COMPDIR)/$$t"; \
 	done
+	@echo "==> XDG profile for shared users -> $(SYSCONFDIR)/pkgusr/skel-u_xdg"
+	$(INSTALL) -d $(SYSCONFDIR)/pkgusr/skel-u_xdg
+	@if [ -f "$(SYSCONFDIR)/pkgusr/skel-u_xdg/.bash_profile" ]; then \
+	    echo "    kept your existing .bash_profile"; \
+	else \
+	    $(INSTALL) -m 644 skel-u_xdg/.bash_profile \
+	        "$(SYSCONFDIR)/pkgusr/skel-u_xdg/.bash_profile"; \
+	    echo "    installed (XDG_RUNTIME_DIR is filled in on first use)"; \
+	fi
 	@echo "==> last build step -> $(SYSCONFDIR)/lfs"
 	$(INSTALL) -d $(SYSCONFDIR)/lfs
 	@if [ -f "$(SYSCONFDIR)/lfs/$(LAST_STEP)" ]; then \
@@ -77,6 +89,7 @@ uninstall:
 	@echo ""
 	@echo "Left alone (they are yours):"
 	@echo "    $(SYSCONFDIR)/lfs/$(LAST_STEP)"
+	@echo "    $(SYSCONFDIR)/pkgusr/skel-u_xdg/.bash_profile"
 	@echo "    /usr/share/lfs        books, settings, snapshots"
 	@echo "    /etc/pkgusr           packagemanager settings"
 
diff --git a/README.md b/README.md
index 0ffd5e7..da3f241 100644
--- a/README.md
+++ b/README.md
@@ -39,11 +39,15 @@ Works **offline** from a downloaded copy of the books.
 ## Install
 
 ```sh
-make install          # to /usr/local
-make install PREFIX=/usr
+make install                        # to /usr
+make install PREFIX=/usr/local
+make install DESTDIR=/mnt/lfs       # into a tree, not the running system
 make uninstall
 ```
 
+`/usr` is the default: the chroot invokes these by name, and
+`packagemanager` runs `packagemanager_install` off `$PATH`.
+
 Installs the four tools, the completion script, and `last_build_step.sh`.
 
 **Requires:** Python 3.9+, bash, coreutils, tar. Book parsing needs
@@ -88,9 +92,18 @@ Separate from package users. An account a program **runs as**, so a browser
 cannot read your files:
 
 ```sh
-packagemanager user create firefox --shared --launcher
+packagemanager user create firefox --shared --share-dir --launcher
 ```
 
+| Flag | Does |
+|---|---|
+| `--shared` | Joins the group on your XDG runtime dir, and links `/etc/pkgusr/skel-u_xdg/.bash_profile` — display, session bus, audio |
+| `--share-dir` | A directory both accounts can write, linked into your home |
+| `--launcher` | A wrapper in `~/bin` that runs the program as that account |
+
+The session group is read from the runtime directory, not assumed. Set it up
+once for your own account; every shared user then joins whatever is there.
+
 ---
 
 ## Repair
diff --git a/packagemanager b/packagemanager
index 2b6546b..81a0fee 100644
--- a/packagemanager
+++ b/packagemanager
@@ -3574,8 +3574,13 @@ Creating the account is the easy part; what makes it usable is the rest:
   * `su - u_<name>` from your account without a password, where the hint's
     /etc/pam.d/su_u_user is installed (skipped where it is not: su then
     simply asks for a password)
-  * with --shared, a directory both accounts can write, symlinked into your
-    home -- how you get files in and out
+  * with --shared, the account joins the group on your XDG runtime directory
+    -- the display, the session bus and audio live there, so a graphical
+    program cannot draw anything without it.  That group is read from the
+    directory, not assumed: it is set up once for you, and every shared
+    account afterwards joins whatever is there.
+  * with --share-dir, a directory both accounts can write, symlinked into
+    your home -- how you get files in and out
   * with --launcher, a wrapper in ~/bin so running `<name>` starts the
     program as that account
 
@@ -3584,9 +3589,16 @@ grant access to:
 
     packagemanager config --main-user <your-login>
 
-Example:
+Everything here is idempotent, and can be applied to an account that already
+exists -- including one made by hand:
 
-    packagemanager user create firefox --shared --launcher --groups audio,video
+    packagemanager user setup firefox --shared --launcher --desktop
+
+Examples:
+
+    packagemanager user create firefox --shared --share-dir --launcher --desktop
+    packagemanager user setup  firefox --desktop          # just add the menu entry
+    packagemanager user setup  firefox --shared           # just the session group
 """
 
 
@@ -3627,6 +3639,113 @@ def cmd_user_create(args):
     _setup_application_user(u, args)
 
 
+def _xdg_runtime_dir_of(user):
+    """The main user's XDG runtime directory, however this system names it."""
+    try:
+        import pwd
+        uid = pwd.getpwnam(user).pw_uid
+    except (KeyError, ImportError):
+        return None
+    for cand in (os.environ.get("XDG_RUNTIME_DIR") if user == os.environ.get("USER") else None,
+                 "/run/user/%d" % uid,
+                 "/tmp/xdg-%s" % user,
+                 "/tmp/xdg-%d" % uid):
+        if cand and os.path.isdir(cand):
+            return cand
+    return None
+
+
+def _join_xdg_runtime_group(main, u):
+    """Put an application user in the group that owns the session.
+
+    A "shared user" is one that may reach the main user's XDG runtime
+    directory -- that is where the display socket, the session bus and the
+    audio socket live, so without it a graphical program starts and then
+    cannot draw anything.
+
+    The group is read from the directory rather than assumed: it is set up
+    once for the main user, and every shared account afterwards simply joins
+    whatever is there."""
+    rt = _xdg_runtime_dir_of(main)
+    if not rt:
+        printWarning(f"  no XDG runtime directory found for {main} -- "
+                     f"{u} will not reach the session")
+        printInfo("    (looked for /run/user/<uid> and /tmp/xdg-<user>)")
+        return
+    try:
+        import grp
+        grp_name = grp.getgrgid(os.stat(rt).st_gid).gr_name
+    except (KeyError, ImportError, OSError):
+        printWarning(f"  cannot read the group on {rt}")
+        return
+    if grp_name in (main, "root"):
+        printWarning(f"  {rt} is group '{grp_name}', which is not a shared "
+                     f"group -- {u} was not added")
+        printInfo("    Set up a runtime group for the session first, then "
+                  "re-run this.")
+        return
+    _run(["usermod", "-a", "-G", grp_name, u])
+    printInfo(f"  {u} joined '{grp_name}' (the group on {rt})")
+
+
+XDG_SKEL = "/etc/pkgusr/skel-u_xdg/.bash_profile"
+
+
+def _link_xdg_profile(main, u):
+    """Point a shared user's .bash_profile at the XDG skeleton.
+
+    Being in the session group is necessary but not sufficient: the account
+    also has to KNOW where the session is.  XDG_RUNTIME_DIR, WAYLAND_DISPLAY
+    and the package-user PATH come from this one file, so a shared user
+    without it can reach the socket and still not find it.
+
+    It is a symlink, not a copy: change the file once and every shared account
+    picks it up."""
+    if not os.path.isfile(XDG_SKEL):
+        printWarning(f"  {XDG_SKEL} is missing -- {u} will not find the session")
+        printInfo("    It ships with these tools; reinstall them, or write it "
+                  "by hand.")
+        return
+
+    # The skeleton is installed with a placeholder, because the runtime
+    # directory belongs to whoever is logged in.  Fill it in once.
+    try:
+        text = open(XDG_SKEL).read()
+    except OSError as e:
+        printWarning(f"  could not read {XDG_SKEL}: {e}")
+        return
+    if "@XDG_RUNTIME_DIR@" in text:
+        rt = _xdg_runtime_dir_of(main)
+        if not rt:
+            printWarning(f"  {XDG_SKEL} still has a placeholder and no runtime "
+                         f"directory was found for {main}")
+            printInfo(f"    Set it by hand:  XDG_RUNTIME_DIR in {XDG_SKEL}")
+            return
+        try:
+            with open(XDG_SKEL, "w") as f:
+                f.write(text.replace("@XDG_RUNTIME_DIR@", rt))
+            printInfo(f"  {XDG_SKEL}: XDG_RUNTIME_DIR set to {rt}")
+        except OSError as e:
+            printWarning(f"  could not write {XDG_SKEL}: {e}")
+            return
+
+    home = os.path.join(BASE_DIR, u)
+    target = os.path.join(home, ".bash_profile")
+    if os.path.islink(target) and os.path.realpath(target) == \
+            os.path.realpath(XDG_SKEL):
+        printInfo(f"  {target} already points at the XDG profile")
+        return
+    try:
+        if os.path.exists(target) or os.path.islink(target):
+            os.replace(target, target + ".pkgusr")
+            printInfo(f"    kept the old one as {target}.pkgusr")
+        os.symlink(XDG_SKEL, target)
+        _run(["chown", "-h", f"{u}:{u}", target])
+        printInfo(f"  {target} -> {XDG_SKEL}")
+    except OSError as e:
+        printWarning(f"  could not link {target}: {e}")
+
+
 def _setup_application_user(u, args):
     """Make an application user actually usable from your own account.
 
@@ -3659,8 +3778,16 @@ def _setup_application_user(u, args):
     # 3. su without a password, if this system uses the hint's PAM file
     _allow_su_via_pam(u, main)
 
-    # 4. a shared directory, when asked for
+    # 4. the session: a shared user is one that may reach the main user's
+    #    XDG runtime directory (the display, the session bus, the audio
+    #    socket).  The group is whatever that directory carries -- reading it
+    #    rather than assuming a name, since the prefix is configurable.
     if getattr(args, "shared", False):
+        _join_xdg_runtime_group(main, u)
+        _link_xdg_profile(main, u)
+
+    # 5. a shared directory, when asked for
+    if getattr(args, "share_dir", False):
         share = os.path.join(home, "Shared")
         _run(["mkdir", "-p", share])
         _run(["chown", f"{u}:{u}", share])
@@ -3672,14 +3799,25 @@ def _setup_application_user(u, args):
         if link:
             printInfo(f"    reachable from your account as {link}")
 
-    # 5. a launcher, when the program is on PATH
-    app = getattr(args, "app", None) or args.name
+    # 6. a launcher and a menu entry, when the program is really there
+    app = getattr(args, "app", None) or getattr(args, "name", None) \
+        or u[len(user_prefix()):]
     import shutil as _sh
-    if getattr(args, "launcher", False):
+    want_launcher = getattr(args, "launcher", False)
+    want_desktop = getattr(args, "desktop", False)
+    if want_launcher or want_desktop:
+        # Their original checked `which $app` first and skipped both when it
+        # was missing -- a launcher for a program that is not installed is
+        # just a broken command in your PATH.
         if not _sh.which(app):
-            printWarning(f"  no '{app}' on PATH -- not writing a launcher")
+            printWarning(f"  no '{app}' on PATH -- no launcher or menu entry")
+            printInfo(f"    (install it first, then: packagemanager user "
+                      f"setup {app} --launcher --desktop)")
         else:
-            _write_launcher(main, u, app)
+            if want_launcher:
+                _write_launcher(main, u, app)
+            if want_desktop:
+                _write_desktop_entry(main, u, app)
 
 
 def _home_of(user):
@@ -3727,6 +3865,55 @@ def _allow_su_via_pam(u, main):
         printWarning(f"  could not write {pam}: {e}")
 
 
+def cmd_user_setup(args):
+    """Apply the application-user setup to an account that already exists.
+
+    Everything `user create` does after making the account, so a user made
+    earlier -- or made by hand -- can be brought up to the same state.  Each
+    step is idempotent, so running it again is safe and only fills in what is
+    missing."""
+    u = _user_prefixed(args.name)
+    if not _user_exists(u):
+        printError(f"{u}: no such user.")
+        printInfo(f"  Create it first:  packagemanager user create {args.name}")
+        sys.exit(1)
+    printHeader(f"Set up {u}")
+    print()
+    if args.groups:
+        _add_groups(u, [g for g in re.split(r"[,\s]+", args.groups) if g])
+    _setup_application_user(u, args)
+
+
+def _write_desktop_entry(main, u, app):
+    """A menu entry that launches the program as the application user.
+
+    Written next to the wrapper, so the program appears in the menu and starts
+    under the right account when clicked -- not just from a terminal."""
+    home = _home_of(main)
+    if not home:
+        return
+    appdir = os.path.join(home, ".local", "share", "applications")
+    os.makedirs(appdir, exist_ok=True)
+    path = os.path.join(appdir, f"{app}.desktop")
+    if os.path.exists(path) and not getattr(_write_desktop_entry, "_force", False):
+        printInfo(f"  {path} already exists -- left alone")
+        return
+    launcher = os.path.join(home, "bin", app)
+    with open(path, "w") as f:
+        f.write(
+            "[Desktop Entry]\n"
+            f"Name={app.capitalize()}\n"
+            f"Comment=Runs as the application user {u}\n"
+            f"Exec={launcher} %U\n"
+            "Terminal=false\n"
+            "Type=Application\n"
+            f"Icon={app}\n"
+            f"StartupWMClass={app.capitalize()}\n")
+    os.chmod(path, 0o644)
+    _run(["chown", f"{main}:{main}", path])
+    printInfo(f"  menu entry: {path}")
+
+
 def _write_launcher(main, u, app):
     """A wrapper in ~/bin that runs <app> as the application user."""
     home = _home_of(main)
@@ -5208,12 +5395,35 @@ def main():
     q.add_argument("name")
     q.add_argument("--groups", help="also add these groups, e.g. audio,video")
     q.add_argument("--shared", action="store_true",
-                   help="a directory both accounts can write, linked into "
-                        "your home")
+                   help="a SHARED user: joins the group on your XDG runtime "
+                        "directory, so it can reach the display, the session "
+                        "bus and audio")
+    q.add_argument("--share-dir", action="store_true",
+                   help="also make a directory both accounts can write, "
+                        "linked into your home")
     q.add_argument("--launcher", action="store_true",
                    help="a wrapper in ~/bin that runs the program as this user")
+    q.add_argument("--desktop", action="store_true",
+                   help="also a menu entry, so it launches from the desktop")
     q.add_argument("--app", help="program the launcher runs (default: <name>)")
     q.set_defaults(func=cmd_user_create)
+
+    q = usub.add_parser("setup",
+                        help="apply the same setup to a user that already "
+                             "exists",
+                        description=_APPLICATION_USER_HELP,
+                        formatter_class=argparse.RawDescriptionHelpFormatter)
+    q.add_argument("name")
+    q.add_argument("--groups", help="also add these groups, e.g. audio,video")
+    q.add_argument("--shared", action="store_true",
+                   help="join the group on your XDG runtime directory")
+    q.add_argument("--share-dir", action="store_true",
+                   help="a directory both accounts can write")
+    q.add_argument("--launcher", action="store_true",
+                   help="a wrapper in ~/bin")
+    q.add_argument("--desktop", action="store_true", help="a menu entry")
+    q.add_argument("--app", help="program to launch (default: <name>)")
+    q.set_defaults(func=cmd_user_setup)
     q = usub.add_parser("delete", help="delete an shared user + group + home")
     q.add_argument("name")
     q.add_argument("--run", action="store_true", help="actually delete (default dry-run)")