From ba1f93366d73a48d5d72ae5a130b8a1a193fdab0 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Wed, 19 Aug 2026 16:43:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(depmap):=20Arch=E2=86=92Zeta=20dependency?= =?UTF-8?q?=20name=20mapping=20+=20base-system=20drop=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/depmap/depmap.go | 537 +++++++++++++++++++++++++++++++++ internal/depmap/depmap_test.go | 202 +++++++++++++ 2 files changed, 739 insertions(+) create mode 100644 internal/depmap/depmap.go create mode 100644 internal/depmap/depmap_test.go diff --git a/internal/depmap/depmap.go b/internal/depmap/depmap.go new file mode 100644 index 0000000..daf6058 --- /dev/null +++ b/internal/depmap/depmap.go @@ -0,0 +1,537 @@ +// Package depmap maps Arch Linux dependency relations onto Zeta package +// names. +// +// The name tables (zetapkgs, renames, dropped) are checked-in literals so the +// mapping is hermetic: zetapkgs mirrors the authoritative +// zeta-packages/packages/index.lua "name = ..." entries. +package depmap + +import ( + "fmt" + "strings" +) + +// MapDep maps a single Arch dependency relation to a Zeta dependency string. +// +// Input forms: "name", "name>=1.2", or the optdepend form "name: description". +// It returns ("", false) when the dependency is dropped (base-system packages +// Zeta provides outside the package manager). Every drop, rename, and +// unmapped passthrough fires warn (nil-safe). +func MapDep(spec string, warn func(string)) (string, bool) { + if warn == nil { + warn = func(string) {} + } + + // 1. Optdepend description form "name: description" — keep the + // name+constraint part only. + if i := strings.Index(spec, ": "); i >= 0 { + spec = spec[:i] + } + + // 2. Split the base name from the version constraint. Arch dependency + // names never contain the operator characters, so the first one found + // starts the constraint. + name, constraint := splitConstraint(spec) + // Zeta's vercmp.parse_dep accepts "=" but normalizes it to "==" + // internally; do the same so emitted specs match Zeta convention. + constraint = normalizeConstraint(constraint) + + // 3. Verbatim match against the Zeta package index → passthrough. + if _, ok := zetapkgs[name]; ok { + return name + constraint, true + } + + // 4. Known Arch→Zeta rename (case fixes included) → rename, keep the + // constraint. + if to, ok := renames[name]; ok { + warn(fmt.Sprintf("renamed dependency %q -> %q", name, to)) + return to + constraint, true + } + + // 5. Base-system packages Zeta has no package for → drop. + if _, ok := dropped[name]; ok { + warn(fmt.Sprintf("dropping base-system dependency %q", name)) + return "", false + } + + // 6. Not in Zeta at all → passthrough loudly. + warn(fmt.Sprintf("unmapped dependency %q (passed through)", name+constraint)) + return name + constraint, true +} + +// splitConstraint returns the base package name and the raw constraint +// ("", ">=1.2", ...). The split happens at the first operator character +// ('<', '>', '=', '~'); Arch package names never contain these. +func splitConstraint(spec string) (name, constraint string) { + for i := 0; i < len(spec); i++ { + switch spec[i] { + case '<', '>', '=', '~': + return strings.TrimSpace(spec[:i]), spec[i:] + } + } + return strings.TrimSpace(spec), "" +} + +// normalizeConstraint rewrites a bare "=" operator to "==" (Zeta's canonical +// form) and leaves every other operator untouched. +func normalizeConstraint(constraint string) string { + trimmed := strings.TrimLeft(constraint, " \t") + if strings.HasPrefix(trimmed, "=") && !strings.HasPrefix(trimmed, "==") { + return constraint[:len(constraint)-len(trimmed)] + "==" + trimmed[1:] + } + return constraint +} + +// zetapkgs is the set of every Zeta package name, checked in verbatim from +// zeta-packages/packages/index.lua ("name = ..." entries). +var zetapkgs = map[string]struct{}{ + "a52dec": {}, + "acl": {}, + "adwaita-icon-theme": {}, + "adwaita-icon-theme-legacy": {}, + "alacritty": {}, + "alsa-lib": {}, + "aquamarine": {}, + "atkmm": {}, + "at-spi2-core": {}, + "avahi": {}, + "bitstreamvera": {}, + "brotli": {}, + "btop": {}, + "bubblewrap": {}, + "bzip2": {}, + "cairo": {}, + "cairomm": {}, + "caja": {}, + "cava": {}, + "cjson": {}, + "cmatrix": {}, + "composefs": {}, + "cpio": {}, + "ctwm": {}, + "dav1d": {}, + "dbus": {}, + "dbus-glib": {}, + "dejavusans": {}, + "discord": {}, + "doas": {}, + "double-conversion": {}, + "edelib": {}, + "egl": {}, + "eom": {}, + "exo": {}, + "expat": {}, + "extra-cmake-modules": {}, + "faad2": {}, + "fastfetch": {}, + "fd": {}, + "femboysay": {}, + "fftw3": {}, + "file": {}, + "firefox": {}, + "fish": {}, + "flac": {}, + "flatpak": {}, + "fltk": {}, + "fmt": {}, + "fontconfig": {}, + "freetype": {}, + "fribidi": {}, + "fvwm3": {}, + "garcon": {}, + "gdk-pixbuf": {}, + "git": {}, + "glib": {}, + "glibmm": {}, + "gmp": {}, + "gnupg": {}, + "gnutls": {}, + "gobject-introspection": {}, + "gpgme": {}, + "graphene": {}, + "gsettings-desktop-schemas": {}, + "gtk3": {}, + "gtk4": {}, + "gtk-layer-shell": {}, + "gtkmm3": {}, + "gtkmm4": {}, + "gtksourceview4": {}, + "gzip": {}, + "harfbuzz": {}, + "hello": {}, + "hicolor-icon-theme": {}, + "htop": {}, + "hwdata": {}, + "hyprgraphics": {}, + "hyprland": {}, + "hyprlang": {}, + "hyprpaper": {}, + "hyprtoolkit": {}, + "hyprutils": {}, + "hyprwayland-scanner": {}, + "hyprwire": {}, + "icu": {}, + "imlib2": {}, + "iniparser": {}, + "iso-codes": {}, + "jam": {}, + "json-c": {}, + "jsoncpp": {}, + "kidletime": {}, + "kwindowsystem": {}, + "labwc": {}, + "larp": {}, + "layer-shell-qt": {}, + "lcms2": {}, + "leancrypto": {}, + "less": {}, + "libappstream": {}, + "libarchive": {}, + "libassuan": {}, + "libb2": {}, + "libcanberra": {}, + "libcurl": {}, + "libdaemon": {}, + "libdbusmenu-lxqt": {}, + "libdconf": {}, + "libdisplay-info": {}, + "libdrm": {}, + "libebml": {}, + "libepoxy": {}, + "libevdev": {}, + "libevent": {}, + "libexif": {}, + "libffi": {}, + "libfm-extra": {}, + "libfm-qt": {}, + "libfontenc": {}, + "libfuse3": {}, + "libfyaml": {}, + "libgcrypt": {}, + "libglvnd": {}, + "libgpg-error": {}, + "libgtop": {}, + "libgudev": {}, + "libICE": {}, + "libidn2": {}, + "libinput": {}, + "libjpeg-turbo": {}, + "libjson-glib": {}, + "libksba": {}, + "liblxqt": {}, + "libmatekbd": {}, + "libmateweather": {}, + "libmatroska": {}, + "libmtdev": {}, + "libnl": {}, + "libnotify": {}, + "libogg": {}, + "libostree": {}, + "libpciaccess": {}, + "libpeas": {}, + "libpng": {}, + "libpsl": {}, + "libqtxdg": {}, + "libreoffice": {}, + "libreoffice-core": {}, + "libreoffice-help": {}, + "libreoffice-share": {}, + "librsvg": {}, + "libseccomp": {}, + "libshout": {}, + "libsigc++": {}, + "libsigc++2": {}, + "libSM": {}, + "libsndfile": {}, + "libsoup2": {}, + "libsoup3": {}, + "libtasn1": {}, + "libtdb": {}, + "libtheora": {}, + "libtinfo": {}, + "libtirpc": {}, + "libudev": {}, + "libunistring": {}, + "libuuid": {}, + "libva": {}, + "libvorbis": {}, + "libvpx": {}, + "libwacom": {}, + "libwebp": {}, + "libwnck3": {}, + "libX11": {}, + "libXau": {}, + "libXaw": {}, + "libxcb": {}, + "libxcb-xrm": {}, + "libXcomposite": {}, + "libXcursor": {}, + "libxcvt": {}, + "libXdamage": {}, + "libXdmcp": {}, + "libXext": {}, + "libxfce4ui": {}, + "libxfce4util": {}, + "libxfce4windowing": {}, + "libXfixes": {}, + "libXfont2": {}, + "libXft": {}, + "libXi": {}, + "libXinerama": {}, + "libXkbfile": {}, + "libxklavier": {}, + "libxml2": {}, + "libxmlb": {}, + "libXmu": {}, + "libXpm": {}, + "libXpresent": {}, + "libXrandr": {}, + "libXrender": {}, + "libXres": {}, + "libxshmfence": {}, + "libxss": {}, + "libXt": {}, + "libXtst": {}, + "libXvMC": {}, + "libXxf86vm": {}, + "libyaml": {}, + "libz": {}, + "lm-sensors": {}, + "lua51": {}, + "luajit": {}, + "luv": {}, + "lxqt": {}, + "lxqt-about": {}, + "lxqt-build-tools": {}, + "lxqt-config": {}, + "lxqt-globalkeys": {}, + "lxqt-menu-data": {}, + "lxqt-notificationd": {}, + "lxqt-panel": {}, + "lxqt-policykit": {}, + "lxqt-powermanagement": {}, + "lxqt-qtplugin": {}, + "lxqt-runner": {}, + "lxqt-session": {}, + "lxqt-themes": {}, + "lz4": {}, + "make": {}, + "mango": {}, + "marco": {}, + "mate": {}, + "mate-backgrounds": {}, + "mate-calc": {}, + "mate-control-center": {}, + "mate-desktop": {}, + "mate-icon-theme": {}, + "mate-menus": {}, + "mate-notification-daemon": {}, + "mate-panel": {}, + "mate-polkit": {}, + "mate-power-manager": {}, + "mate-session-manager": {}, + "mate-settings-daemon": {}, + "mate-terminal": {}, + "mate-utils": {}, + "md4c": {}, + "menu-cache": {}, + "mesa": {}, + "mesa-dri-gallium": {}, + "mesa-gl": {}, + "micro": {}, + "mksh": {}, + "mm-common": {}, + "mpc": {}, + "mpfr": {}, + "mpg123": {}, + "muparser": {}, + "neovim": {}, + "nettle": {}, + "nghttp2": {}, + "npth": {}, + "nscde": {}, + "nss": {}, + "ntbtls": {}, + "nvidia": {}, + "nvidia-compiler": {}, + "nvidia-cuda": {}, + "nvidia-firmware": {}, + "nvidia-kernel": {}, + "nvidia-optix": {}, + "nvidia-utils": {}, + "openbox": {}, + "opencode": {}, + "openssl": {}, + "opsec": {}, + "opus": {}, + "p11-kit": {}, + "pango": {}, + "pangomm": {}, + "pcmanfm-qt": {}, + "pcre2": {}, + "pekwm": {}, + "pinentry": {}, + "pipewire": {}, + "pixman": {}, + "plasma-wayland-protocols": {}, + "pluma": {}, + "polkit": {}, + "polkit-qt-1": {}, + "procps-ng": {}, + "pugixml": {}, + "pulseaudio": {}, + "qt5": {}, + "qtbase": {}, + "qtdeclarative": {}, + "qterminal": {}, + "qtermwidget": {}, + "qtshadertools": {}, + "qtsvg": {}, + "qttools": {}, + "qtwayland": {}, + "ripgrep": {}, + "rofi": {}, + "rsync": {}, + "scenefx": {}, + "sdl2": {}, + "seatd": {}, + "setxkbmap": {}, + "shared-mime-info": {}, + "solid": {}, + "spdlog": {}, + "speex": {}, + "spirv-tools": {}, + "sqlite": {}, + "startup-notification": {}, + "swaybg": {}, + "taglib": {}, + "tango-icon-theme": {}, + "tar": {}, + "thunar": {}, + "tllist": {}, + "tree": {}, + "tree-sitter": {}, + "tslib": {}, + "twolame": {}, + "unibilium": {}, + "unzip": {}, + "upower": {}, + "util-macros": {}, + "vim": {}, + "vlc": {}, + "vte": {}, + "vulkan-headers": {}, + "waybar": {}, + "wayland": {}, + "wayland-protocols": {}, + "wget": {}, + "which": {}, + "wlroots": {}, + "wlr-protocols": {}, + "wmaker": {}, + "wofi": {}, + "xauth": {}, + "xcb-proto": {}, + "xcb-util": {}, + "xcb-util-cursor": {}, + "xcb-util-image": {}, + "xcb-util-keysyms": {}, + "xcb-util-renderutil": {}, + "xcb-util-wm": {}, + "xdg-desktop-portal": {}, + "xdg-user-dirs": {}, + "xdpyinfo": {}, + "xf86-input-libinput": {}, + "xf86-video-intel": {}, + "xfce4": {}, + "xfce4-appfinder": {}, + "xfce4-dev-tools": {}, + "xfce4-notifyd": {}, + "xfce4-panel": {}, + "xfce4-power-manager": {}, + "xfce4-session": {}, + "xfce4-settings": {}, + "xfce4-terminal": {}, + "xfconf": {}, + "xfdesktop": {}, + "xfwm4": {}, + "xgamma": {}, + "xinit": {}, + "xkbcommon": {}, + "xkbcomp": {}, + "xkeyboard-config": {}, + "xmessage": {}, + "xmodmap": {}, + "xorg-full": {}, + "xorgproto": {}, + "xorg-server": {}, + "xprop": {}, + "xrandr": {}, + "xrdb": {}, + "xset": {}, + "xshmfence": {}, + "xterm": {}, + "xtrans": {}, + "xwayland": {}, + "xz-utils": {}, + "zip": {}, + "zsh": {}, + "zstd": {}, +} + +// renames maps Arch Linux package names to their Zeta counterparts. Sources +// are Arch's canonical (lowercase) spelling; targets are the verbatim names +// in zetapkgs. The libX* entries are pure case fixes (Arch spells the X11 +// family lowercase); libxcb and friends are already correct in Zeta and +// deliberately absent. +var renames = map[string]string{ + "zlib": "libz", + "xz": "xz-utils", + "freetype2": "freetype", + "libxkbcommon": "xkbcommon", + "libice": "libICE", + "libsm": "libSM", + "libx11": "libX11", + "libxau": "libXau", + "libxaw": "libXaw", + "libxcomposite": "libXcomposite", + "libxcursor": "libXcursor", + "libxdamage": "libXdamage", + "libxdmcp": "libXdmcp", + "libxext": "libXext", + "libxfixes": "libXfixes", + "libxfont2": "libXfont2", + "libxft": "libXft", + "libxi": "libXi", + "libxinerama": "libXinerama", + "libxkbfile": "libXkbfile", + "libxmu": "libXmu", + "libxpm": "libXpm", + "libxpresent": "libXpresent", + "libxrandr": "libXrandr", + "libxrender": "libXrender", + "libxres": "libXres", + "libxt": "libXt", + "libxtst": "libXtst", + "libxvmc": "libXvMC", + "libxxf86vm": "libXxf86vm", +} + +// dropped lists Arch base-system packages that Zeta provides outside the +// package manager and therefore has no package for (mirroring the dbus and +// libudev stubs in the index). +var dropped = map[string]struct{}{ + "glibc": {}, + "gcc-libs": {}, + "gcc": {}, + "bash": {}, + "coreutils": {}, + "filesystem": {}, + "linux-api-headers": {}, + "systemd": {}, + "util-linux": {}, + "ncurses": {}, + "readline": {}, + "tzdata": {}, + "ca-certificates": {}, +} diff --git a/internal/depmap/depmap_test.go b/internal/depmap/depmap_test.go new file mode 100644 index 0000000..da18aac --- /dev/null +++ b/internal/depmap/depmap_test.go @@ -0,0 +1,202 @@ +package depmap + +import ( + "strings" + "testing" +) + +func TestMapDep(t *testing.T) { + tests := []struct { + name string + spec string + want string + wantOK bool + warns int + warnMsg string // exact expected warn message ("" = only check count) + }{ + { + name: "zlib renames to libz", + spec: "zlib", + want: "libz", + wantOK: true, + warns: 1, + }, + { + name: "glibc dropped as base system", + spec: "glibc", + want: "", + wantOK: false, + warns: 1, + warnMsg: `dropping base-system dependency "glibc"`, + }, + { + name: "verbatim index name keeps constraint", + spec: "pcre2>=10.42", + want: "pcre2>=10.42", + wantOK: true, + warns: 0, + }, + { + name: "optdepend description stripped", + spec: "python: for scripting", + want: "python", + wantOK: true, + warns: 1, + }, + { + name: "unknown dep passes through with warn", + spec: "someunknownpkg123", + want: "someunknownpkg123", + wantOK: true, + warns: 1, + warnMsg: `unmapped dependency "someunknownpkg123" (passed through)`, + }, + { + name: "rename preserves constraint", + spec: "zlib>=1.3.1", + want: "libz>=1.3.1", + wantOK: true, + warns: 1, + }, + { + name: "x11 case fix libx11", + spec: "libx11", + want: "libX11", + wantOK: true, + warns: 1, + }, + { + name: "xz renames to xz-utils", + spec: "xz", + want: "xz-utils", + wantOK: true, + warns: 1, + }, + { + name: "x11 case fix libice", + spec: "libice", + want: "libICE", + wantOK: true, + warns: 1, + }, + { + name: "freetype2 renames to freetype", + spec: "freetype2", + want: "freetype", + wantOK: true, + warns: 1, + }, + { + name: "libxkbcommon renames to xkbcommon", + spec: "libxkbcommon", + want: "xkbcommon", + wantOK: true, + warns: 1, + }, + { + name: "libxcb already correct not renamed", + spec: "libxcb", + want: "libxcb", + wantOK: true, + warns: 0, + }, + { + name: "zeta casing input passes through verbatim", + spec: "libX11", + want: "libX11", + wantOK: true, + warns: 0, + }, + { + name: "bare = normalized to ==", + spec: "foo=1.0", + want: "foo==1.0", + wantOK: true, + warns: 1, + }, + { + name: "description plus constraint plus rename", + spec: "zlib>=1.2: for compression", + want: "libz>=1.2", + wantOK: true, + warns: 1, + }, + { + name: "drop applies regardless of constraint", + spec: "glibc>=2.38", + want: "", + wantOK: false, + warns: 1, + }, + { + name: "gcc-libs dropped", + spec: "gcc-libs", + want: "", + wantOK: false, + warns: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var warns []string + got, ok := MapDep(tt.spec, func(msg string) { + warns = append(warns, msg) + }) + + if got != tt.want { + t.Errorf("MapDep(%q) = %q, want %q", tt.spec, got, tt.want) + } + if ok != tt.wantOK { + t.Errorf("MapDep(%q) ok = %v, want %v", tt.spec, ok, tt.wantOK) + } + if len(warns) != tt.warns { + t.Errorf("MapDep(%q) fired %d warns (%v), want %d", + tt.spec, len(warns), warns, tt.warns) + } + if tt.warnMsg != "" && len(warns) == 1 && warns[0] != tt.warnMsg { + t.Errorf("MapDep(%q) warn = %q, want %q", tt.spec, warns[0], tt.warnMsg) + } + }) + } +} + +// TestMapDepNilWarn ensures MapDep tolerates a nil warn callback. +func TestMapDepNilWarn(t *testing.T) { + got, ok := MapDep("zlib", nil) + if got != "libz" || !ok { + t.Fatalf("MapDep(zlib, nil) = %q, %v; want %q, true", got, ok, "libz") + } +} + +// TestRenameTargetsExistInIndex guards the checked-in tables against typos: +// every rename target must be a real Zeta package, and every dropped name must +// NOT be one. +func TestRenameTargetsExistInIndex(t *testing.T) { + for from, to := range renames { + if _, ok := zetapkgs[to]; !ok { + t.Errorf("rename %q -> %q: target not in zetapkgs index", from, to) + } + if _, ok := zetapkgs[from]; ok { + t.Errorf("rename %q -> %q: source already in zetapkgs (rename is dead code)", from, to) + } + if _, ok := dropped[to]; ok { + t.Errorf("rename %q -> %q: target also in drop list", from, to) + } + } + for name := range dropped { + if _, ok := zetapkgs[name]; ok { + t.Errorf("drop %q: present in zetapkgs index (drop is wrong)", name) + } + } +} + +// TestRenamesSortedLint is a cheap self-check that rename sources are +// lowercase (Arch convention) so case fixes are keyed from the Arch spelling. +func TestRenamesLowercaseKeys(t *testing.T) { + for from := range renames { + if strings.ToLower(from) != from { + t.Errorf("rename key %q is not lowercase", from) + } + } +}