ci: run the full unit suite on windows (#5370)

* ci: run the full unit suite on windows via make unit

Drops the curated `unit:windows` subset; the windows job now bootstraps like linux and runs `make unit`. Exceptions get added as failures show up.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: skip fixture refresh for windows unit tests

binny has no windows release, so the bootstrap action can't install the tools the fixture refresh needs.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test: skip linux container fixtures when docker can't run them

Windows CI runners only run windows containers, so image fixtures go through `testutils.SkipWithoutLinuxContainers`. No behavior change on linux/mac.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: close leaked file handles and posix squashfs paths on windows

- output writers are now closed on early returns in `scan`/`convert` (open handles block cleanup on windows)
- `newSBOMMultiWriter` used `path.Dir` on host paths, so output subdirectories were never created on windows
- squashfs walk paths were built with `filepath.Join`
- skip a few posix-only test cases (dir perms, xdg fallback, go-rpmdb ndb handle leak upstream)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(golang): resolve go source module dir natively on windows

- `parseGoModFile` passed the resolver's posix (`/c/...`) path to the go toolchain, so go source resolution silently found nothing on windows
- skip container-built fixtures (go archs, java builds) and golden image tarballs on windows; stereoscope's layer cache filenames contain `:`, which windows rejects

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: keep posix paths posix on windows

- `ChrootContext.ToChrootPath` nested volumes (`D:\cwd\C:\root`) when the scan root was on another drive, so the prefix was never trimmed
- bitnami, python wheel/egg, and alpm parsers used `filepath` on posix location paths
- alpm backup-file lookups silently missed on windows

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ci: pull the test fixture cache for windows unit tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: don't map virtual file tree paths through the host on windows

`FiletreeResolver` without a chroot (snaps) ran request paths through `filepath.Abs`, rooting them on the current volume (`/payload.txt` -> `/d/payload.txt`), so snap scans on windows found nothing.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: match tar entry globs as posix on windows

- `matchesAnyGlob` used `doublestar.PathMatch` (host separator) on tar entry names, so tar-wrapped java archives silently produced nothing on windows
- test helpers now treat binary snippet keys and `FromFile` locations as posix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate windows path fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate go-rpmdb close fix

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* close squashfs fixture writers before finalizing

- windows dir listings can report a zero size for files still open, so the manifest was packed empty

(also adds a temporary windows diagnostic for the binary cataloger)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* temp: surface windows binary cataloger diagnostics

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: keep directory symlink targets posix on windows

- the directory indexer joined symlink targets with native separators, so links in the posix file tree pointed nowhere and globs on link names found nothing
- binary fixture logical keys used native separators, so nested snippets were picked as the fixture dir on windows

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* keep addSymlinkToIndex under the funlen limit

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* drop windows skips covered by the stereoscope and go-rpmdb bumps

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-10-05 10:41:03 -04:00
committed by GitHub
parent 6d2493bae4
commit 6c9bb732dd
60 changed files with 391 additions and 135 deletions
+4
View File
@@ -114,6 +114,10 @@ func RunConvert(ctx context.Context, opts *ConvertOptions, userInput string) err
if err != nil {
return err
}
// output files are opened up front; release them if we bail out before writing
if c, ok := writer.(io.Closer); ok {
defer func() { _ = c.Close() }()
}
}
for _, output := range unchanged {
+5
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"
@@ -181,6 +182,10 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
if err != nil {
return err
}
// output files are opened up front; release them if we bail out before writing
if c, ok := writer.(io.Closer); ok {
defer func() { _ = c.Close() }()
}
sources := opts.From
if len(sources) == 0 {
+10 -3
View File
@@ -4,6 +4,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -22,9 +23,10 @@ func Test_defaultDir(t *testing.T) {
homeDir := filepath.Join(tmpDir, "fake-home")
tests := []struct {
name string
env map[string]string
expected string
name string
env map[string]string
expected string
skipOnWindows bool
}{
{
name: "no-xdg",
@@ -32,6 +34,8 @@ func Test_defaultDir(t *testing.T) {
"HOME": homeDir,
},
expected: homeDir,
// xdg falls back to LOCALAPPDATA on windows, not HOME
skipOnWindows: true,
},
{
name: "xdg-cache",
@@ -59,6 +63,9 @@ func Test_defaultDir(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.skipOnWindows && runtime.GOOS == "windows" {
t.Skip("posix-only cache dir fallback")
}
restoreCache(t)
for k, v := range test.env {
+28 -4
View File
@@ -2,10 +2,11 @@ package options
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
@@ -20,7 +21,10 @@ import (
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.Writer = (*sbomMultiWriter)(nil)
var _ interface {
io.Closer
sbom.Writer
} = (*sbomMultiWriter)(nil)
var _ interface {
io.Closer
@@ -164,6 +168,12 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
}
out := &sbomMultiWriter{}
defer func() {
// don't leak the files already opened when a later output fails
if err != nil {
_ = out.Close()
}
}()
for _, option := range options {
switch len(option.Path) {
@@ -172,8 +182,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
format: option.Format,
})
default:
// create any missing subdirectories
dir := path.Dir(option.Path)
// create any missing subdirectories (option.Path is a host path, so this must be filepath)
dir := filepath.Dir(option.Path)
if dir != "" {
s, err := os.Stat(dir)
if err != nil {
@@ -210,6 +220,20 @@ func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
return errs
}
// Close releases any files held by the writers. Write already closes each file it writes to, so this matters
// when the SBOM is never written (e.g. an error after the writer was created).
func (m *sbomMultiWriter) Close() error {
var errs error
for _, w := range m.writers {
if c, ok := w.(io.Closer); ok {
if err := c.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
errs = multierror.Append(errs, err)
}
}
}
return errs
}
// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
type sbomStreamWriter struct {
format sbom.FormatEncoder
+2 -2
View File
@@ -202,9 +202,9 @@ func Test_newSBOMMultiWriter(t *testing.T) {
if test.err {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
}
require.NoError(t, err)
t.Cleanup(func() { _ = mw.Close() })
assert.Len(t, mw.writers, len(test.expected))