Filter same-version rebuilds from pkg_outdated - #1351
Conversation
… is clicked (#14918) Addresses to #14294 (together with posit-dev/ark#1351) The Packages pane persists outdated-package state to an on-disk cache so update indicators can render immediately on a warm start. The toolbar Refresh button reused that cache so that within the freshness window (default 24h) it re-rendered the cached indicators instead of recomputing, so an explicit Refresh could keep showing stale "outdated" flags that no longer matched the environment. Only the less-discoverable "Refresh Metadata" command forced a live recompute. This makes the user-initiated Refresh authoritative. It now always recomputes outdated state live for every package, bypassing the freshness window, while automatic warm-start refreshes (on session attach) keep using the fast cached path. A forced recompute overwrites stale `outdated: true` entries with the current result, so clicking Refresh reliably clears indicators that no longer apply. This is the Positron half of #14294. The complementary Ark change (posit-dev/ark#1351) stops `pkg_outdated` from flagging same-version packages that were merely rebuilt under a new R minor, which was the original trigger for the inflated counts. ### Release Notes #### New Features - N/A #### Bug Fixes - Packages pane: the Refresh button now recomputes outdated packages live instead of showing stale cached indicators (#14294) ### Validation Steps @:packages-pane The bug only surfaces when the cache disagrees with reality while installed versions stay fixed, so the reliable way to exercise it is to change the configured repositories (the cache is keyed on the interpreter, not the repos) and confirm the pane tracks a live `old.packages()` after Refresh. It's pretty fiddly! 1. Start an R session and point at a snapshot recent enough that most installed packages are current: ```r options(repos = c(CRAN = "https://packagemanager.posit.co/cran/latest")) ``` 2. Open the Packages pane, click the Refresh button, and note the outdated count. Confirm it matches ground truth from the console: ```r nrow(old.packages()) ``` 3. Without reinstalling anything, point at an older dated snapshot so some installed versions are now behind: ```r options(repos = c(CRAN = "https://packagemanager.posit.co/cran/2024-06-01")) ``` 4. Click Refresh again. The pane's outdated indicators should update to match the new `nrow(old.packages())`, even though only the repos changed. Before this fix, within the freshness window the pane would keep showing the step-2 result from the cache. The Python equivalent is the same shape: install a package pinned below its latest version (`pip install "requests<2.31"`), Refresh so the pane caches it as outdated, upgrade it in the console (`pip install -U requests`), and confirm Refresh clears the indicator to match `pip list --outdated`.
|
@bricestacey, I believe Jenny is OOO now and we'd like to get this fix in before we branch for 2026.08. Can you take a look and review, at least based on what your understanding is? Thanks! |
|
@lionel-, I am having a bit of trouble getting this PR reviewed. 😅 We do want to get this fix in for this milestone, so could you take a look at it? We believe there may be further work we can do here for packages with compiled code that actually should be marked outdated in some situations, but this is a first step to improve what people are seeing. |
lionel-
left a comment
There was a problem hiding this comment.
LG although a real integration test would be great
| # Turn an `old.packages()` matrix into the pane's outdated-package list. Split | ||
| # out from the RPC so the filtering and formatting can be tested without | ||
| # querying live repositories. | ||
| # | ||
| # We keep only packages whose repository version is strictly greater than the | ||
| # installed version. old.packages() also flags packages whose repository copy | ||
| # has the *same* version but a newer build or publication date (see the | ||
| # `needs.install` closure inside utils::old.packages). Right after a new R | ||
| # minor is installed, CRAN's freshly rebuilt binaries carry newer Built dates | ||
| # than the user's carried-over packages, so hundreds of same-version packages | ||
| # get flagged even though no newer version exists. The pane's update indicator | ||
| # means "a newer version is available", so a same-version rebuild is not an | ||
| # update we should surface. |
There was a problem hiding this comment.
Does this simpler comment (to move below near the check) retain all useful information?
# Since R 4.6, `old.packages()` also reports equal-version packages with newer
# `Built` or `Published` timestamps. The pane promises a newer package version,
# so exclude those entries.| @@ -0,0 +1,107 @@ | |||
| // | |||
| // packages_pane.rs | |||
There was a problem hiding this comment.
I'd love an integration test that exercises the format produced by the R version on CI.
Claude thinks this is feasible. If you have time, could you take a look?
A real integration test is cheap
No network, no `R CMD INSTALL`, no compilation. `installed.packages()` only needs `<lib>/<pkg>/Meta/package.rds` holding a list with `$DESCRIPTION` and `$Built$R` (`utils:::.readPkgDesc`), and `available.packages()` only needs a DCF `PACKAGES` file under `<repo>/src/contrib`. Both are a few `saveRDS()`/`cat()` calls, and the run is instant — the whole thing is much cheaper than the current six r_task processes.
```r
lib <- tempfile("lib"); repo <- tempfile("repo")
dir.create(file.path(repo, "src", "contrib"), recursive = TRUE)
install_fake <- function(pkg, version, built) {
dir.create(file.path(lib, pkg, "Meta"), recursive = TRUE)
saveRDS(
list(
DESCRIPTION = c(Package = pkg, Version = version, Built = built),
Built = list(R = package_version(sub("^R ([0-9.]+).*", "\\1", built)))
),
file.path(lib, pkg, "Meta", "package.rds")
)
}
install_fake("rebuild", "1.5.0", "R 4.5.2; ; 2026-01-01 00:00:00 UTC; unix")
install_fake("upgrade", "1.0.0", "R 4.6.0; ; 2026-05-01 00:00:00 UTC; unix")
# PACKAGES: rebuild 1.5.0 with a newer `Built:` date, upgrade 2.0.0
options(
repos = c(CRAN = paste0("file:///", normalizePath(repo, winslash = "/"))),
pkgType = "source",
# `Built` is only standard for binary repos, so ask for it explicitly and
# the fixture stays portable instead of faking a mac.binary layout.
available_packages_fields = "Built"
)
.libPaths(c(lib, .libPaths()))
.ps.rpc.pkg_outdated()
```There was a problem hiding this comment.
Done! Thanks for that idea, which works pretty much as written, with just some adjustments for older R.
The one thing worth a look is the version gating. On R < 4.6 old.packages() never flags the same-version rebuild, so "the rebuild is absent from the result" would pass vacuously on most of our matrix. The tests now assert that old.packages() flags it exactly when getRversion() >= "4.6.0", so the fixture fails loudly if it ever stops reproducing the 4.6 behavior instead of going quiet.
Verified passing against R 4.2.1, 4.4.2, and 4.6.0 locally, and confirmed both tests fail if I relax the filter to >=.
This addresses part of posit-dev/positron#14294, where the packages pane surfaces a large number of packages as having updates available when in fact they don't.
.ps.rpc.pkg_outdated()was returning everything fromutils::old.packages(). That function flags a package as outdated when the repository version is strictly newer or when the version is identical but the repository copy has a newer build or publication date (see theneeds.installclosure insideutils:::old.packages). Right after a new R minor is installed, CRAN's freshly rebuilt binaries carry newerBuiltdates than the user's carried-over packages, so hundreds of same-version packages get flagged even though no newer version exists.The pane's update indicator means "a newer version is available," so this filters
old.packages()down to packages whose repository version is strictly greater than the installed version, dropping same-version rebuilds.Changes
.ps.rpc.pkg_outdated()into a purepkg_outdated_result()helper so it can be tested without querying live repositories.ReposVer > Installed(compared withpackage_version(), so10.0.0correctly beats9.0.0).Testing
Added integration tests in
crates/ark/tests/integration/packages_pane.rsdrivingpkg_outdated_result()with fabricatedold.packages()matrices: a genuine upgrade is kept, a same-version rebuild is dropped, a mixed list keeps only real upgrades in order, versions compare numerically rather than as strings, andNULL/ all-rebuild inputs return an empty list.Also verified manually that
old.packages()really does flag a same-version rebuild (via injectedinstPkgs/availablematrices with a newerBuiltdate) and that the filter drops it while keeping a genuine upgrade.Positron Release Notes
New Features
Bug Fixes