This commit is contained in:
Copilot 2026-07-02 09:47:51 +00:00 committed by GitHub
commit 6f410de124
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 207 additions and 10 deletions

View file

@ -54,13 +54,13 @@ jobs:
- name: Verify
if: |
steps.execute.outputs.cache-hit != 'false' ||
steps.execute.outputs.all-package-version-list != 'fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.2,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1'
steps.execute.outputs.all-package-version-list != 'fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.3,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1'
run: |
echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}"
echo "package-version-list = ${{ steps.execute.outputs.package-version-list }}"
echo "all-package-version-list = ${{ steps.execute.outputs.all-package-version-list }}"
echo "diff all-package-version-list"
diff <(echo "${{ steps.execute.outputs.all-package-version-list }}" ) <(echo "fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.2,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1")
diff <(echo "${{ steps.execute.outputs.all-package-version-list }}" ) <(echo "fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.3,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1")
exit 1
shell: bash

View file

@ -68,3 +68,12 @@ func TestNormalizedList_VirtualPackagesExists_StdoutsConcretePackage(t *testing.
result := cmdtesting.New(t, createReplayLogs).Run("normalized-list", "libvips")
result.ExpectSuccessfulOut("libvips42=8.9.1-2")
}
// Regression test for https://github.com/awalsh128/cache-apt-pkgs-action/issues/218:
// a real package (ghostscript) that apt-cache show reports as purely virtual on stale apt lists
// must resolve to itself (ghostscript=<version>), not to a mangled section-header token such as
// "Reverse=Provides:".
func TestNormalizedList_RealPackageMisidentifiedAsVirtual_StdoutsCorrectPackage(t *testing.T) {
result := cmdtesting.New(t, createReplayLogs).Run("normalized-list", "ghostscript")
result.ExpectSuccessfulOut("ghostscript=10.02.1~dfsg1-0ubuntu7.8")
}

View file

@ -0,0 +1,19 @@
2025/03/15 22:29:14 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:14 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show ghostscript",
"Stdout": "",
"Stderr": "N: Can't select candidate version from package ghostscript as it has no candidate\nN: Can't select versions from package 'ghostscript' as it is purely virtual\nN: No packages found\n",
"CombinedOut": "N: Can't select candidate version from package ghostscript as it has no candidate\nN: Can't select versions from package 'ghostscript' as it is purely virtual\nN: No packages found\n",
"ExitCode": 0
}
EXECUTION-OBJ-END
2025/03/15 22:29:14 EXECUTION-OBJ-START
{
"Cmd": "apt-cache showpkg ghostscript",
"Stdout": "Package: ghostscript\nVersions: \n10.02.1~dfsg1-0ubuntu7.8 (/var/lib/dpkg/info/ghostscript.list)\n Description Language: \n File: /var/lib/apt/lists/example\n MD5: ab0ac55fd3cdba396944cd65f6d36654\n\nReverse Depends: \n cups,ghostscript\nDependencies: \n10.02.1~dfsg1-0ubuntu7.8 - libgs10 (5 10.02.1~dfsg1-0ubuntu7.8) libc6 (2 2.34)\nProvides: \n10.02.1~dfsg1-0ubuntu7.8 - postscript-viewer (= ) ghostscript-x (= 10.02.1~dfsg1-0ubuntu7.8)\nReverse Provides: \n",
"Stderr": "",
"CombinedOut": "Package: ghostscript\nVersions: \n10.02.1~dfsg1-0ubuntu7.8 (/var/lib/dpkg/info/ghostscript.list)\n Description Language: \n File: /var/lib/apt/lists/example\n MD5: ab0ac55fd3cdba396944cd65f6d36654\n\nReverse Depends: \n cups,ghostscript\nDependencies: \n10.02.1~dfsg1-0ubuntu7.8 - libgs10 (5 10.02.1~dfsg1-0ubuntu7.8) libc6 (2 2.34)\nProvides: \n10.02.1~dfsg1-0ubuntu7.8 - postscript-viewer (= ) ghostscript-x (= 10.02.1~dfsg1-0ubuntu7.8)\nReverse Provides: \n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -32,6 +32,9 @@ func isErrLine(line string) bool {
}
// Resolves virtual packages names to their concrete one.
// Falls back to returning the package itself (with version from the Versions section) when the
// Reverse Provides section is empty, which handles real packages that are mistakenly treated as
// virtual due to stale apt lists.
func getNonVirtualPackage(executor exec.Executor, name string) (pkg *AptPackage, err error) {
execution := executor.Exec("apt-cache", "showpkg", name)
err = execution.Error()
@ -40,28 +43,81 @@ func getNonVirtualPackage(executor exec.Executor, name string) (pkg *AptPackage,
return pkg, err
}
const reverseProvides = "Reverse Provides:"
const versions = "Versions:"
inVersions := false
inReverseProvides := false
var firstVersion string
for _, line := range strings.Split(execution.CombinedOut, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if trimmed == "Reverse Provides:" {
inReverseProvides = true
if strings.HasPrefix(trimmed, "W: ") || isErrLine(trimmed) {
continue
}
if !inReverseProvides || strings.HasPrefix(trimmed, "W: ") || isErrLine(trimmed) {
if trimmed == versions {
inVersions = true
inReverseProvides = false
continue
}
if trimmed == reverseProvides {
inReverseProvides = true
inVersions = false
continue
}
// Handle "Reverse Provides: <name> <version>" format where the header and first entry
// appear on the same line. If the inline content is parseable (≥2 words), return it
// immediately. If it is malformed or absent (< 2 words), treat the line as a plain
// section header so that subsequent lines are still scanned for entries.
if strings.HasPrefix(trimmed, reverseProvides+" ") {
entry := strings.TrimSpace(trimmed[len(reverseProvides)+1:])
if entry != "" {
// Split into at most 3 parts: name, version, and any trailing metadata (e.g. "(= )")
splitLine := GetSplitLine(entry, " ", 3)
if len(splitLine.Words) >= 2 {
return &AptPackage{Name: splitLine.Words[0], Version: splitLine.Words[1]}, nil
}
}
// No parseable entry on this line; fall through to entry-scanning mode so
// subsequent lines in this section are still examined.
inReverseProvides = true
inVersions = false
continue
}
if strings.HasSuffix(trimmed, ":") {
break
}
splitLine := GetSplitLine(trimmed, " ", 3)
if len(splitLine.Words) < 2 {
// Some other section header — reset tracking flags.
inVersions = false
inReverseProvides = false
continue
}
return &AptPackage{Name: splitLine.Words[0], Version: splitLine.Words[1]}, nil
// Capture the first version listed for this package (used as fallback below).
// Split into at most 2 parts: version and the file-path metadata that follows.
if inVersions && firstVersion == "" {
splitLine := GetSplitLine(trimmed, " ", 2)
if len(splitLine.Words) >= 1 && splitLine.Words[0] != "" {
firstVersion = splitLine.Words[0]
}
}
if inReverseProvides {
// Split into at most 3 parts: name, version, and any trailing metadata (e.g. "(= )").
splitLine := GetSplitLine(trimmed, " ", 3)
if len(splitLine.Words) < 2 {
continue
}
return &AptPackage{Name: splitLine.Words[0], Version: splitLine.Words[1]}, nil
}
}
// If no Reverse Provides entries were found but the package itself has a known version,
// it is a real (non-virtual) package that was mistakenly treated as virtual (e.g. because
// apt-cache show was run against stale lists). Return it directly.
if firstVersion != "" {
return &AptPackage{Name: name, Version: firstVersion}, nil
}
return pkg, fmt.Errorf("unable to parse reverse provides package name and version from apt-cache showpkg output below:\n%s", execution.CombinedOut)
}

View file

@ -51,3 +51,116 @@ func TestGetNonVirtualPackage_WithWarningsInReverseProvides(t *testing.T) {
t.Fatalf("unexpected package.\nexpected: %+v\nactual: %+v", expected, *pkg)
}
}
// Regression test for https://github.com/awalsh128/cache-apt-pkgs-action/issues/218
// A real package (ghostscript) that is mis-identified as purely virtual due to stale apt lists
// has an empty "Reverse Provides:" section in apt-cache showpkg. getNonVirtualPackage must
// fall back to the "Versions:" section and return the package itself rather than failing or
// returning the section header as the package name.
func TestGetNonVirtualPackage_WithRealPackageMisidentifiedAsVirtual(t *testing.T) {
executor := mockExecutor{
executions: map[string]*execpkg.Execution{
"apt-cache showpkg ghostscript": {
Cmd: "apt-cache showpkg ghostscript",
CombinedOut: strings.Join([]string{
"Package: ghostscript",
"Versions: ",
"10.02.1~dfsg1-0ubuntu7.8 (/var/lib/dpkg/info/ghostscript.list)",
" Description Language: ",
" File: /var/lib/apt/lists/example",
" MD5: abc123",
"",
"Reverse Depends: ",
" cups,ghostscript",
"Dependencies: ",
"10.02.1~dfsg1-0ubuntu7.8 - libgs10 (5 10.02.1~dfsg1-0ubuntu7.8)",
"Provides: ",
"10.02.1~dfsg1-0ubuntu7.8 - postscript-viewer (= ) ghostscript-x (= 10.02.1~dfsg1-0ubuntu7.8)",
"Reverse Provides: ",
"",
}, "\n"),
ExitCode: 0,
},
},
}
pkg, err := getNonVirtualPackage(executor, "ghostscript")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pkg == nil {
t.Fatal("expected package but got nil")
}
expected := AptPackage{Name: "ghostscript", Version: "10.02.1~dfsg1-0ubuntu7.8"}
if *pkg != expected {
t.Fatalf("unexpected package.\nexpected: %+v\nactual: %+v", expected, *pkg)
}
}
// Regression test: when the "Reverse Provides:" header and entry appear on the same line
// (e.g. "Reverse Provides: provider 1.0"), the entry must be parsed correctly and the
// section header must not be treated as the package name.
func TestGetNonVirtualPackage_WithInlineReverseProvides(t *testing.T) {
executor := mockExecutor{
executions: map[string]*execpkg.Execution{
"apt-cache showpkg somevirtual": {
Cmd: "apt-cache showpkg somevirtual",
CombinedOut: strings.Join([]string{
"Package: somevirtual",
"Versions: ",
"",
"Reverse Provides: concrete-pkg 2.0.0 (= 2.0.0)",
"",
}, "\n"),
ExitCode: 0,
},
},
}
pkg, err := getNonVirtualPackage(executor, "somevirtual")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pkg == nil {
t.Fatal("expected package but got nil")
}
expected := AptPackage{Name: "concrete-pkg", Version: "2.0.0"}
if *pkg != expected {
t.Fatalf("unexpected package.\nexpected: %+v\nactual: %+v", expected, *pkg)
}
}
// Edge case: "Reverse Provides: " header with no parseable inline entry falls back to
// scanning subsequent lines, then to the Versions fallback if still no entries are found.
func TestGetNonVirtualPackage_WithInlineHeaderNoEntry(t *testing.T) {
executor := mockExecutor{
executions: map[string]*execpkg.Execution{
"apt-cache showpkg realpkg": {
Cmd: "apt-cache showpkg realpkg",
CombinedOut: strings.Join([]string{
"Package: realpkg",
"Versions: ",
"1.2.3 (/var/lib/dpkg/info/realpkg.list)",
"Reverse Provides: ",
"",
}, "\n"),
ExitCode: 0,
},
},
}
pkg, err := getNonVirtualPackage(executor, "realpkg")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pkg == nil {
t.Fatal("expected package but got nil")
}
expected := AptPackage{Name: "realpkg", Version: "1.2.3"}
if *pkg != expected {
t.Fatalf("unexpected package.\nexpected: %+v\nactual: %+v", expected, *pkg)
}
}