A GTK4/libadwaita application store for Gentoo's Portage package manager, plus a terminal companion that shares the same core.
Search and install packages, watch builds stream live in a job queue, and get told about the things Gentoo otherwise expects you to remember to check yourself — security advisories, unread news, pending config updates, a stale tree, orphaned packages — without running five different commands after every sync.
portage-store # the GUI
portage-store-cli # the same core, no window
Status: works, used daily on the author's own system, but young. The privilege model (see below) is deliberately documented rather than hidden — read it before installing.
Screenshots · Features · Requirements · Installing · Privilege model · The CLI · Architecture · Development · Packaging
- Whole-tree in-memory index. The first search parses one full
eix --xmldump (~22k packages) and keeps it. Everything after that filters memory: ~600 ms once, ~2 ms per search thereafter. Dropped and rebuilt only when a sync completes. - Name and description search, so "password" finds
keepassandbitwarden-desktop-bin, not just packages with "password" in the name. - Typo tolerance. When nothing matches well, a Levenshtein pass over the index suggests close names instead of returning an empty page.
- Search operators —
cat:games-*,use:wayland,installed:true,@world— applied in memory, so changing one re-renders instantly. - Curated category tiles — each showing a real app icon (Steam for Games, GIMP or Blender for Create, ...) picked at random from that category's pool on every launch, with the tile's own gradient drawn from that icon's color — above a landing page of themed carousels.
- A single job queue. Portage takes a global lock, so one
emergeruns at a time and everything else waits its turn — reorderable and cancellable from a popover without touching what's already running. The whole queue can be exported as a POSIX shell script (oneemerge/helper invocation per queued job, in run order), so what's queued can be handed to a different machine,cron, or just kept as a record instead of only ever running through this app. - Automatic conflict resolution. A failed build that needs a USE flag, a keyword, a license acceptance, or a circular-dependency break is detected, explained, and offered as apply-and-retry.
- Toolchain blast-radius warning. Before running an update that touches
gcc,glibc, or another toolchain component, a confirmation warns that this commonly cascades into rebuilding everything linked against it, if free RAM also looks tight for that at the current job count — one of several preflight checks (alongside a stale tree and unreviewed config updates) shown before a big@worldupdate, none of which block it; they're every one dismissed together by choosing Continue. - Live build output, with a log drawer, an errors-only filter, and a searchable history of past builds.
- Build-time ETAs from
qlop's own recorded merge history, counting down live. - Resource throttling (on by default): builds run under
nice/ionicewith a RAM-awareMAKEOPTScap instead of whatever's inmake.conf. Added after an unthrottled build genuinely OOM-killed unrelated processes during development. - Night-only builds (opt-in): a mutating job queued outside the 23:00–07:00 window waits instead of starting immediately, with a "Run Now" override always available if it turns out you don't want to wait after all.
- Sandbox builds. For a package the live system's resolver won't touch (a hard mask, a keyword mask, a conflict) — builds it instead in a disposable, throttled chroot: a shared stage3 base (fetched and checksum-verified once, so a second sandbox build never re-downloads it) with the live portage tree bind-mounted read-only and up to three pooled overlayfs instances, so a package that conflicts with whatever was already built in one instance just overflows to a fresh one instead of failing outright. The result lands in the same binary-package cache the host uses, so it's installable afterward like any other cached binpkg.
- Downgrades, installed straight from a locally cached binary package
(
--usepkgonly, so it can never silently fall back to rebuilding from source) — no network round-trip, no recompile.
- GLSA security advisories via
glsa-check— the one category no default Gentoo install surfaces anywhere unless you already know the tool exists. Shown in its own section above ordinary updates, with a one-click fix. - Unread Gentoo news (GLEP 42), pending
CONFIG_PROTECTupdates with an inline diff reviewer and bulk accept, sync staleness, preserved-rebuild, and orphaned packages. - A health dashboard combining all of it, with optional periodic background checks and trend history ("this has been pending 6 days").
- Dependency preview, download size, and compile-vs-prebuilt — cached by atom+version, disk-backed with a 1-hour TTL so it survives a restart.
- Reverse dependencies — "why is this installed?" walks up to an
@worldancestor, bounded on depth, fan-out, and total subprocess calls so one click onglibcan't spawn thousands. - USE flag editor showing where each flag's current value actually comes from.
- Man page, upstream AppStream metadata, GitHub social card, and Gentoo wiki/Bugzilla mentions, when they exist.
- Presets — a named bundle of USE flags plus a starter package list ("Gaming Desktop", "Minimal Server") that layers onto what's already installed. Export to JSON, import someone else's.
- Profile bundles — the whole picture:
@worldplus every file under/etc/portage, tarred up. Importing shows a diff of what would actually change before touching anything.
Overlays (enable GURU and friends), make.conf variables, package.env overrides,
kernel selection, profile switching, and binary-package repositories — each a focused
page over its own module.
Every write this app makes under /etc/portage is committed to a git repo there,
browsable and revertible from the GUI.
A second, fully independent job lane. Flatpak has no global lock, so a Flatpak update
never waits behind an hours-long @world rebuild. Flatpak-only search hits appear as a
collapsed strip beneath the Portage results, never mixed into them.
Build: Rust 1.85+ (2024 edition; developed against 1.96), gui-libs/gtk 4.12+,
gui-libs/libadwaita 1.5+.
Runtime:
| Tool | Package | Used for |
|---|---|---|
emerge, portageq, glsa-check |
sys-apps/portage |
installs, updates, advisories |
eix |
app-portage/eix |
the search index (run eix-update first) |
equery |
app-portage/gentoolkit |
reverse dependencies |
qlop |
app-portage/portage-utils |
build-time history / ETAs |
eclean-dist, eclean-pkg |
app-portage/gentoolkit |
cache cleanup |
eselect |
app-admin/eselect |
unread news, kernel symlink selection |
doas |
app-admin/doas |
the privilege helper (see below) |
curl, git, tar, df, man |
base system | artwork, config history, bundles |
flatpak |
sys-apps/flatpak |
optional — the Flatpak lane auto-disables without it |
git clone https://github.com/tarilka0gg/portage-store.git
cd portage-store
cargo build --releaseProduces target/release/portage-store (GUI) and target/release/portage-store-cli.
The in-app web preview is behind an off-by-default feature flag, because WebKitGTK is
one of the slowest packages on Gentoo to build and this app does not need it — links
open in your browser instead. Enable it with --features webview if you want it.
This is required — without it, anything that writes to the system will fail.
# 1. Install the helper scripts, root-owned and not writable by your user
sudo mkdir -p /usr/local/libexec/portage-store
sudo install -m 0755 -o root -g root resources/priv-helper.sh \
/usr/local/libexec/portage-store/priv-helper
sudo install -m 0755 -o root -g root resources/sandbox-build.sh \
/usr/local/libexec/portage-store/sandbox-build.sh
# 2. Create its audit log
sudo mkdir -p /var/log/portage-store
sudo chown root:portage /var/log/portage-store
sudo chmod 0750 /var/log/portage-store
sudo touch /var/log/portage-store/priv-helper.log
sudo chown root:portage /var/log/portage-store/priv-helper.log
sudo chmod 0640 /var/log/portage-store/priv-helper.log
# 3. Allow it to run without a password, for your user only
echo "permit nopass $USER cmd /usr/local/libexec/portage-store/priv-helper" \
| sudo tee -a /etc/doas.confThen install the binaries wherever you like (~/.local/bin, /usr/local/bin, …).
Everything privileged goes through one root-owned script, reachable by a
passwordless doas rule scoped to exactly that one path.
This is a real, deliberate tradeoff, so it's stated plainly:
- There is no password prompt. A passwordless rule means nothing stands between "the app asked for this" and "it happened as root."
- In exchange, the attack surface is made as narrow as possible. No privileged call
ever carries script text. The helper is invoked with a subcommand name and
arguments — never
bash -c "$SOMETHING"— and the script itself lives root-owned on disk, referenced only by name. - The helper does not trust its caller. Every path is re-validated on the root side:
it must resolve (after canonicalization, closing symlink-swap races) under
/etc/portageor/etcas appropriate, and the runnable-binary list is an allowlist, not a filter. - Every privileged call is logged, by root, to
/var/log/portage-store/priv-helper.log— the compensating control for having no prompt. The GUI has a page to read it.
Read resources/priv-helper.sh before installing. It is deliberately written to be read.
Same core, no window. Every subcommand is a thin wrapper over the same functions the GUI calls — not a reimplementation.
portage-store-cli search firefox
portage-store-cli show www-client/firefox
portage-store-cli list-installed
portage-store-cli updates
portage-store-cli install app-misc/tmux --pretend
portage-store-cli install app-misc/tmux
portage-store-cli uninstall app-misc/tmux
portage-store-cli update-world --pretend
portage-store-cli sync
portage-store-cli preset list
portage-store-cli preset apply "Minimal Server"
portage-store-cli preset apply-file ./my-preset.jsonMutating commands stream emerge output live and exit non-zero on failure, so they
compose fine in scripts.
Three pieces, one direction of dependency:
portage-store (GTK4 GUI) portage-store-cli (clap)
\ /
\ /
portage_store (lib) ← no GTK, no UI state
|
eix · emerge · qlop · glsa-check · flatpak
doas+helper · /etc/portage · /var/db/pkg
src/portage/,src/backend.rs,src/flatpak.rs— the domain layer. Everything that knows how Gentoo works: parsingeix --xml, buildingemergeargv, reading@world, writingpackage.use. Plain functions returning plain structs. Zerogtk/adwreferences anywhere in it, which is exactly what made the CLI cheap to add rather than a rewrite.src/ui/— all the GTK. Split by area (queue.rs,checks.rs,browse.rs,flatpak_lane.rs,log_drawer.rs, one file per dialog), each taking&Rc<App>.src/bin/portage-store-cli.rs— the terminal front end.
Where to look, by feature:
| Module | Owns |
|---|---|
eix.rs |
tree index, search, category browse, lookup |
emerge.rs |
job builders, streaming runner, pretend cache, output parsers |
priv_write.rs, command.rs |
the one privileged-write seam, and its test harness |
glsa.rs, news.rs, config_protect.rs, sync.rs |
the four health banners |
reverse_deps.rs |
"why is this installed", with its own recursion budget |
qlop.rs, resource_limits.rs |
build-time history and throttling math |
preset.rs, profile_bundle.rs |
the two "share my setup" shapes |
media.rs, icons.rs |
the two on-disk artwork caches |
cargo build
cargo test # 255 tests, no live system required
cargo runTests never touch a real subprocess: parsers are fed captured fixtures, and the two
places that genuinely interleave subprocess calls with logic (priv_write.rs,
reverse_deps.rs) go through a CommandRunner trait with a fake implementation. So
cargo test passes on a non-Gentoo machine.
packaging/ holds a work-in-progress ebuild, metadata.xml, .desktop entry,
AppStream metainfo, and a placeholder icon, aimed at eventual submission to
GURU. Not submitted, and not yet
correct — packaging/README.md lists exactly what needs fixing first, including a
real helper-path mismatch between the ebuild and the compiled-in constant.
GPL-2.0, matching Portage and Gentoo's own licensing.













