Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package melange for openSUSE:Factory checked in at 2026-04-02 17:42:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Thu Apr 2 17:42:46 2026 rev:150 rq:1344282 version:0.48.0 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2026-03-30 18:35:50.114436402 +0200 +++ /work/SRC/openSUSE:Factory/.melange.new.21863/melange.changes 2026-04-02 17:43:55.294185582 +0200 @@ -1,0 +2,16 @@ +Wed Apr 01 04:27:02 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.48.0: + * sca: Adjust generate{Perl,Ruby}Deps to account for non-usrmerge + (#2454) + * build(deps): bump the actions group across 1 directory with 2 + updates (#2452) + * build(deps): bump the gomod group across 1 directory with 4 + updates (#2451) + * sca: Generate Perl dependency (#2453) + * feat(qemu): optional build observability event logging (#2394) + * build(deps): bump github.com/go-git/go-git/v5 from 5.17.0 to + 5.17.1 (#2449) + * qemu: resolve vmlinuz symlinks and try to fix them (#2445) + +------------------------------------------------------------------- Old: ---- melange-0.47.0.obscpio New: ---- melange-0.48.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.phWb2k/_old 2026-04-02 17:43:56.410231855 +0200 +++ /var/tmp/diff_new_pack.phWb2k/_new 2026-04-02 17:43:56.410231855 +0200 @@ -17,7 +17,7 @@ Name: melange -Version: 0.47.0 +Version: 0.48.0 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.phWb2k/_old 2026-04-02 17:43:56.446233347 +0200 +++ /var/tmp/diff_new_pack.phWb2k/_new 2026-04-02 17:43:56.450233513 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/melange.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v0.47.0</param> + <param name="revision">refs/tags/v0.48.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.phWb2k/_old 2026-04-02 17:43:56.502235670 +0200 +++ /var/tmp/diff_new_pack.phWb2k/_new 2026-04-02 17:43:56.514236167 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/chainguard-dev/melange</param> <param name="changesrevision">3f6115b820985d70ca3c93cdf8519c1b3b4cfe81</param></service><service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/melange.git</param> - <param name="changesrevision">3fa79327ebf36fb4b3700d6a053b95836e45ccd8</param></service></servicedata> + <param name="changesrevision">54faa684108e9d1e0a716adc3819e105da614bf7</param></service></servicedata> (No newline at EOF) ++++++ melange-0.47.0.obscpio -> melange-0.48.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/go.mod new/melange-0.48.0/go.mod --- old/melange-0.47.0/go.mod 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/go.mod 2026-03-31 23:10:54.000000000 +0200 @@ -3,16 +3,16 @@ go 1.25.7 require ( - chainguard.dev/apko v1.1.16 + chainguard.dev/apko v1.2.0 github.com/chainguard-dev/clog v1.8.0 github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10 - github.com/chainguard-dev/yam v0.2.53 + github.com/chainguard-dev/yam v0.2.54 github.com/charmbracelet/log v1.0.0 github.com/docker/cli v29.3.1+incompatible github.com/docker/docker v28.5.2+incompatible github.com/dprotaso/go-yit v0.0.0-20250513224043-18a80f8f6df4 github.com/github/go-spdx/v2 v2.4.0 - github.com/go-git/go-git/v5 v5.17.0 + github.com/go-git/go-git/v5 v5.17.2 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.21.3 github.com/google/licenseclassifier/v2 v2.0.0 @@ -47,16 +47,14 @@ gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.13.0 - sigs.k8s.io/release-utils v0.12.3 + sigs.k8s.io/release-utils v0.12.4 sigs.k8s.io/yaml v1.6.0 ) require ( - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/fatih/color v1.18.0 // indirect github.com/google/martian/v3 v3.3.3 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect @@ -78,7 +76,7 @@ require ( chainguard.dev/go-grpc-kit v0.17.16 // indirect - chainguard.dev/sdk v0.1.51 // indirect + chainguard.dev/sdk v0.1.52 // indirect cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect @@ -123,7 +121,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect @@ -133,7 +131,6 @@ github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -172,9 +169,9 @@ golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - google.golang.org/api v0.272.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/api v0.273.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 gopkg.in/warnings.v0 v0.1.2 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/go.sum new/melange-0.48.0/go.sum --- old/melange-0.47.0/go.sum 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/go.sum 2026-03-31 23:10:54.000000000 +0200 @@ -1,9 +1,9 @@ -chainguard.dev/apko v1.1.16 h1:edh6bjoFW2Ysyj15NhTG1LWWtpByUgUmIjdmLmeTIj8= -chainguard.dev/apko v1.1.16/go.mod h1:Ls6WmnS+JeXUMM/Zo4LV/qu3cxZA25CjYf88llinJl8= +chainguard.dev/apko v1.2.0 h1:44uI/EBIjMuOwdwxDCbIpY1bJrSOLEVtRqM9SUcz69E= +chainguard.dev/apko v1.2.0/go.mod h1:mLYcD45WyC55MGyjeJSju1+FmxzZFB9LCAnZzbRp8WU= chainguard.dev/go-grpc-kit v0.17.16 h1:Y9RKwZCnrYR3S0K8BiazyOoBrZF+Q7bJWDacfKXz2zg= chainguard.dev/go-grpc-kit v0.17.16/go.mod h1:0vrfIBJguXNa+EKOUEhx1Fj2aBp8o6A8gAHoidiF8ps= -chainguard.dev/sdk v0.1.51 h1:rB2rBqExsz6ydbEbMlK/Lm90PSik57lQ40sQlmu1yAs= -chainguard.dev/sdk v0.1.51/go.mod h1:EFyAQrI1kR73wQVmOFzBr473Ka3gXkzzXuvjJaHuNcM= +chainguard.dev/sdk v0.1.52 h1:G1wmZHU8v5E78YlCHuwQH0Hwt4NBBCvCNAFad5FUanQ= +chainguard.dev/sdk v0.1.52/go.mod h1:IZIiUyuNaxTao6mpC/BROsw/dwjl/DmCR/raIT7eK4c= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= @@ -44,8 +44,8 @@ github.com/chainguard-dev/clog v1.8.0/go.mod h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo= github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10 h1:XR2vgQC024I9/boh9r1ihVv8Z14+pbvWqXeYMCnZJpc= github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10/go.mod h1:1p6+MesLcjKeON5BRWa7I87mvAY0QmKjgginIM3w6BI= -github.com/chainguard-dev/yam v0.2.53 h1:98wzR0hTZmOu8yl77/D+U9vO4ZB2TbTeq0deD3Zqy8s= -github.com/chainguard-dev/yam v0.2.53/go.mod h1:Sbt8pVO8DbHoVly44oF5gg03NRxl9AhEImOkqGyoQCs= +github.com/chainguard-dev/yam v0.2.54 h1:QIzSAllLHUCx0s3Ot1PpdcY7c6jhiWAEP6SAqOIN6xs= +github.com/chainguard-dev/yam v0.2.54/go.mod h1:Sbt8pVO8DbHoVly44oF5gg03NRxl9AhEImOkqGyoQCs= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -59,10 +59,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -131,8 +129,8 @@ github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= -github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= +github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= @@ -190,8 +188,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= -github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -488,17 +486,17 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= -google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/api v0.273.0 h1:r/Bcv36Xa/te1ugaN1kdJ5LoA5Wj/cL+a4gj6FiPBjQ= +google.golang.org/api v0.273.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= -google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= +google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -545,7 +543,7 @@ mvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -sigs.k8s.io/release-utils v0.12.3 h1:iNVJY81QfmMCmXxMg8IvvkkeQNk6ZWlLj+iPKSlKyVQ= -sigs.k8s.io/release-utils v0.12.3/go.mod h1:BvbNmm1BmM3cnEpBmNHWL3wOSziOdGlsYR8vCFq/Q0o= +sigs.k8s.io/release-utils v0.12.4 h1:kuG6WTWGCKx5uUrJwl2uFErOKOw+4Ba8WrPmOQh5J3g= +sigs.k8s.io/release-utils v0.12.4/go.mod h1:Tc3iM9DVM3W9oJu/6rEI+LnREuhy8lZ7wInQhRBtUoo= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/build/build.go new/melange-0.48.0/pkg/build/build.go --- old/melange-0.47.0/pkg/build/build.go 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/pkg/build/build.go 2026-03-31 23:10:54.000000000 +0200 @@ -770,6 +770,16 @@ } log.Infof("retrieved and wrote post-build workspace to: %s", b.WorkspaceDir) + // Retrieve and log build observability events if the observability hook + // is installed. Only applicable to QEMU builds which run in a full VM. + if b.Runner.Name() == container.QemuName { + if obsEvents, err := container.RetrieveObservabilityEvents(ctx, cfg); err != nil { + log.Warnf("failed to retrieve observability events: %v", err) + } else if obsEvents != nil { + container.LogObservabilityEvents(ctx, obsEvents) + } + } + // perform package linting for _, lt := range linterQueue { log.Infof("running package linters for %s", lt.pkgName) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/container/observability.go new/melange-0.48.0/pkg/container/observability.go --- old/melange-0.47.0/pkg/container/observability.go 1970-01-01 01:00:00.000000000 +0100 +++ new/melange-0.48.0/pkg/container/observability.go 2026-03-31 23:10:54.000000000 +0200 @@ -0,0 +1,232 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package container + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/chainguard-dev/clog" +) + +// Well-known paths where the observability framework may write events. +// The hook package determines which path is used; we probe all of them. +var observabilityEventPaths = []string{ + "/var/log/observability/events.json", + "/opt/observability-logs/events.log", + "/tmp/observability/events.log", +} + +// ObservabilityEvents holds parsed event data retrieved from the build VM. +type ObservabilityEvents struct { + // RawData is the raw NDJSON event data. + RawData []byte + + // EventCount is the total number of events. + EventCount int + + // NetworkConnections is the list of network connections observed. + NetworkConnections []NetworkConnection +} + +// NetworkConnection represents a single observed network connection. +type NetworkConnection struct { + Process string `json:"process"` + Protocol string `json:"protocol"` + SrcAddr string `json:"src_addr"` + SrcPort uint32 `json:"src_port"` + DstAddr string `json:"dst_addr"` + DstPort uint32 `json:"dst_port"` + Family string `json:"family"` + Function string `json:"function"` // tcp_connect, tcp_close, etc. + Timestamp string `json:"timestamp"` +} + +// RetrieveObservabilityEvents fetches observability events from the build VM +// via the SSHControlClient (port 2223, unchrooted root access). This should +// be called after the build completes but before TerminatePod. +// +// Returns nil with no error if the observability hook is not installed or no +// events were generated. This makes the feature fully optional — default +// builds without the hook are completely unaffected. +func RetrieveObservabilityEvents(ctx context.Context, cfg *Config) (*ObservabilityEvents, error) { + if cfg.SSHControlClient == nil { + return nil, nil + } + + log := clog.FromContext(ctx) + + // Probe known event file locations. If none exist, the observability + // hook is not installed and we silently return nil. + eventsPath := "" + for _, path := range observabilityEventPaths { + err := sendSSHCommand(ctx, cfg.SSHControlClient, cfg, nil, nil, nil, false, + []string{"test", "-f", path}) + if err == nil { + eventsPath = path + break + } + } + + if eventsPath == "" { + // No events file found — observability hook is not installed. + // This is the normal case for default builds; return silently. + return nil, nil + } + + log.Infof("qemu: found observability events at %s", eventsPath) + + // Cap retrieval to prevent a pathological VM from exhausting host memory. + // 50 MiB is well above normal builds (9-17k events ≈ a few MiB). + const maxEventsBytes = 50 << 20 // 50 MiB + var eventsBuf bytes.Buffer + err := sendSSHCommand(ctx, cfg.SSHControlClient, cfg, nil, nil, &eventsBuf, false, + []string{"head", "-c", fmt.Sprintf("%d", maxEventsBytes), eventsPath}) + if err != nil { + return nil, fmt.Errorf("failed to retrieve observability events: %w", err) + } + + rawData := eventsBuf.Bytes() + if len(rawData) == 0 { + log.Warn("qemu: observability events file exists but is empty") + return &ObservabilityEvents{RawData: rawData}, nil + } + + // Parse events to extract network connections + connections, eventCount := extractNetworkConnections(rawData) + + log.Infof("qemu: retrieved %d observability events (%d network connections)", eventCount, len(connections)) + + return &ObservabilityEvents{ + RawData: rawData, + EventCount: eventCount, + NetworkConnections: connections, + }, nil +} + +// LogObservabilityEvents writes all observability events to melange's stdout +// via the structured logger. Each raw event is logged as a separate line with +// an [OBSERVABILITY] prefix for filtering. Network connections get a dedicated +// summary section. In the elastic build environment, these log lines flow to +// Cloud Logging via GKE pod stdout and are individually searchable. +func LogObservabilityEvents(ctx context.Context, events *ObservabilityEvents) { + if events == nil || len(events.RawData) == 0 { + return + } + + log := clog.FromContext(ctx) + + // Summary header + log.Infof("[OBSERVABILITY] === Build Observability Report: %d events, %d network connections ===", + events.EventCount, len(events.NetworkConnections)) + + // Log each network connection (high-value, low-volume) + for _, conn := range events.NetworkConnections { + log.Infof("[OBSERVABILITY] network: %s %s %s:%d -> %s:%d (%s)", + conn.Function, conn.Process, + conn.SrcAddr, conn.SrcPort, + conn.DstAddr, conn.DstPort, + conn.Protocol) + } + + // Log every raw event line (for Cloud Logging ingestion) + for line := range bytes.SplitSeq(events.RawData, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + log.Infof("[OBSERVABILITY] %s", string(line)) + } + + log.Infof("[OBSERVABILITY] === End of observability report ===") +} + +// extractNetworkConnections parses NDJSON observability events and extracts +// network connection information from kprobe events. +func extractNetworkConnections(data []byte) ([]NetworkConnection, int) { + connections := make([]NetworkConnection, 0) + eventCount := 0 + + for line := range bytes.SplitSeq(data, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(line, &raw); err != nil { + continue // Skip malformed lines + } + eventCount++ + + kprobeData, ok := raw["process_kprobe"] + if !ok { + continue + } + + var kprobe struct { + Process struct { + Binary string `json:"binary"` + } `json:"process"` + FunctionName string `json:"function_name"` + Args []struct { + SockArg *struct { + Family string `json:"family"` + Protocol string `json:"protocol"` + SAddr string `json:"saddr"` + DAddr string `json:"daddr"` + SPort uint32 `json:"sport"` + DPort uint32 `json:"dport"` + } `json:"sock_arg,omitempty"` + } `json:"args"` + } + + if err := json.Unmarshal(kprobeData, &kprobe); err != nil { + continue + } + + switch kprobe.FunctionName { + case "tcp_connect", "tcp_close", "tcp_sendmsg": + default: + continue + } + + if len(kprobe.Args) == 0 || kprobe.Args[0].SockArg == nil { + continue + } + + sock := kprobe.Args[0].SockArg + timestamp := "" + if t, ok := raw["time"]; ok { + _ = json.Unmarshal(t, ×tamp) + } + + connections = append(connections, NetworkConnection{ + Process: kprobe.Process.Binary, + Protocol: sock.Protocol, + SrcAddr: sock.SAddr, + SrcPort: sock.SPort, + DstAddr: sock.DAddr, + DstPort: sock.DPort, + Family: sock.Family, + Function: kprobe.FunctionName, + Timestamp: timestamp, + }) + } + + return connections, eventCount +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/container/observability_test.go new/melange-0.48.0/pkg/container/observability_test.go --- old/melange-0.47.0/pkg/container/observability_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/melange-0.48.0/pkg/container/observability_test.go 2026-03-31 23:10:54.000000000 +0200 @@ -0,0 +1,424 @@ +// Copyright 2025 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package container + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/chainguard-dev/clog" + "github.com/chainguard-dev/clog/slogtest" +) + +// ObservabilityEvent represents the JSON event structure emitted by the +// observability framework. Used for event schema validation and test parsing. +type ObservabilityEvent struct { + ProcessExec *ProcessExec `json:"process_exec,omitempty"` + ProcessKprobe *ProcessKprobe `json:"process_kprobe,omitempty"` + ProcessExit *ProcessExit `json:"process_exit,omitempty"` + NodeName string `json:"node_name,omitempty"` + Time string `json:"time,omitempty"` +} + +type ProcessExec struct { + Process *Process `json:"process,omitempty"` + Parent *Process `json:"parent,omitempty"` +} + +type ProcessKprobe struct { + Process *Process `json:"process,omitempty"` + Parent *Process `json:"parent,omitempty"` + FunctionName string `json:"function_name,omitempty"` + Args []KprobeArg `json:"args,omitempty"` + Action string `json:"action,omitempty"` + PolicyName string `json:"policy_name,omitempty"` +} + +type ProcessExit struct { + Process *Process `json:"process,omitempty"` + Parent *Process `json:"parent,omitempty"` +} + +type Process struct { + ExecID string `json:"exec_id,omitempty"` + PID uint32 `json:"pid,omitempty"` + UID uint32 `json:"uid,omitempty"` + CWD string `json:"cwd,omitempty"` + Binary string `json:"binary,omitempty"` + Arguments string `json:"arguments,omitempty"` + Flags string `json:"flags,omitempty"` + StartTime string `json:"start_time,omitempty"` + ParentExecID string `json:"parent_exec_id,omitempty"` +} + +type KprobeArg struct { + SockArg *KprobeSock `json:"sock_arg,omitempty"` + SkbArg *KprobeSkb `json:"skb_arg,omitempty"` + IntArg *int64 `json:"int_arg,omitempty"` +} + +type KprobeSock struct { + Family string `json:"family,omitempty"` + Type string `json:"type,omitempty"` + Protocol string `json:"protocol,omitempty"` + SAddr string `json:"saddr,omitempty"` + DAddr string `json:"daddr,omitempty"` + SPort uint32 `json:"sport,omitempty"` + DPort uint32 `json:"dport,omitempty"` + State string `json:"state,omitempty"` +} + +type KprobeSkb struct { + SAddr string `json:"saddr,omitempty"` + DAddr string `json:"daddr,omitempty"` + SPort uint32 `json:"sport,omitempty"` + DPort uint32 `json:"dport,omitempty"` + Protocol string `json:"protocol,omitempty"` + Family string `json:"family,omitempty"` + Len uint32 `json:"len,omitempty"` +} + +// --- Unit Tests --- + +func TestObservabilityEventParsing(t *testing.T) { + tests := []struct { + name string + input string + wantType string // "exec", "kprobe", "exit" + wantValid bool + }{ + { + name: "process_exec event", + input: `{ + "process_exec": { + "process": { + "binary": "/usr/bin/gcc", + "arguments": "-o hello hello.c", + "pid": 1234 + } + }, + "time": "2025-01-01T00:00:00Z" + }`, + wantType: "exec", + wantValid: true, + }, + { + name: "tcp_connect kprobe event", + input: `{ + "process_kprobe": { + "process": { + "binary": "/usr/bin/curl", + "pid": 5678 + }, + "function_name": "tcp_connect", + "args": [{ + "sock_arg": { + "family": "AF_INET", + "type": "SOCK_STREAM", + "protocol": "IPPROTO_TCP", + "saddr": "10.0.2.15", + "daddr": "104.198.14.52", + "sport": 48272, + "dport": 443 + } + }], + "policy_name": "network-monitor" + }, + "time": "2025-01-01T00:00:01Z" + }`, + wantType: "kprobe", + wantValid: true, + }, + { + name: "tcp_close kprobe event", + input: `{ + "process_kprobe": { + "process": { + "binary": "/usr/bin/wget", + "pid": 9012 + }, + "function_name": "tcp_close", + "args": [{ + "sock_arg": { + "family": "AF_INET", + "type": "SOCK_STREAM", + "protocol": "IPPROTO_TCP", + "daddr": "8.8.8.8", + "dport": 53 + } + }], + "policy_name": "network-monitor" + }, + "time": "2025-01-01T00:00:02Z" + }`, + wantType: "kprobe", + wantValid: true, + }, + { + name: "empty JSON", + input: `{}`, + wantType: "", + wantValid: true, + }, + { + name: "invalid JSON", + input: `{not valid json}`, + wantType: "", + wantValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var event ObservabilityEvent + err := json.Unmarshal([]byte(tt.input), &event) + + if tt.wantValid && err != nil { + t.Fatalf("expected valid JSON, got error: %v", err) + } + if !tt.wantValid && err == nil { + t.Fatalf("expected invalid JSON, got nil error") + } + if !tt.wantValid { + return + } + + switch tt.wantType { + case "exec": + if event.ProcessExec == nil { + t.Error("expected process_exec event, got nil") + } + if event.ProcessExec != nil && event.ProcessExec.Process == nil { + t.Error("expected process_exec.process, got nil") + } + case "kprobe": + if event.ProcessKprobe == nil { + t.Error("expected process_kprobe event, got nil") + } + if event.ProcessKprobe != nil { + if event.ProcessKprobe.FunctionName == "" { + t.Error("expected function_name in kprobe event") + } + if len(event.ProcessKprobe.Args) == 0 { + t.Error("expected args in kprobe event") + } + } + case "": + // Empty event + default: + t.Fatalf("unknown event type: %s", tt.wantType) + } + }) + } +} + +func TestObservabilityNetworkEventExtraction(t *testing.T) { + input := `{ + "process_kprobe": { + "process": { + "binary": "/usr/bin/apk", + "arguments": "add --no-cache gcc", + "pid": 100 + }, + "function_name": "tcp_connect", + "args": [{ + "sock_arg": { + "family": "AF_INET", + "type": "SOCK_STREAM", + "protocol": "IPPROTO_TCP", + "saddr": "10.0.2.15", + "daddr": "packages.wolfi.dev", + "sport": 54321, + "dport": 443 + } + }], + "policy_name": "network-monitor" + }, + "time": "2025-01-01T00:00:00Z" + }` + + var event ObservabilityEvent + if err := json.Unmarshal([]byte(input), &event); err != nil { + t.Fatalf("failed to parse event: %v", err) + } + + if event.ProcessKprobe == nil { + t.Fatal("expected kprobe event") + } + + kp := event.ProcessKprobe + if kp.FunctionName != "tcp_connect" { + t.Errorf("function_name = %q, want %q", kp.FunctionName, "tcp_connect") + } + if len(kp.Args) == 0 || kp.Args[0].SockArg == nil { + t.Fatal("expected sock_arg in args[0]") + } + + sock := kp.Args[0].SockArg + if sock.DPort != 443 { + t.Errorf("dport = %d, want 443", sock.DPort) + } + if sock.Protocol != "IPPROTO_TCP" { + t.Errorf("protocol = %q, want IPPROTO_TCP", sock.Protocol) + } +} + +func TestParseObservabilityEventsFile(t *testing.T) { + eventsData := strings.Join([]string{ + `{"process_exec":{"process":{"binary":"/bin/sh","pid":1}},"time":"2025-01-01T00:00:00Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/wget","pid":2},"function_name":"tcp_connect","args":[{"sock_arg":{"daddr":"1.2.3.4","dport":80,"family":"AF_INET","protocol":"IPPROTO_TCP"}}],"policy_name":"network-monitor"},"time":"2025-01-01T00:00:01Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/apk","pid":3},"function_name":"tcp_connect","args":[{"sock_arg":{"daddr":"5.6.7.8","dport":443,"family":"AF_INET","protocol":"IPPROTO_TCP"}}],"policy_name":"network-monitor"},"time":"2025-01-01T00:00:02Z"}`, + `{"process_exit":{"process":{"binary":"/bin/sh","pid":1}},"time":"2025-01-01T00:00:03Z"}`, + }, "\n") + + events, networkEvents, err := parseObservabilityEvents([]byte(eventsData)) + if err != nil { + t.Fatalf("parseObservabilityEvents() error: %v", err) + } + + if len(events) != 4 { + t.Errorf("got %d events, want 4", len(events)) + } + if len(networkEvents) != 2 { + t.Errorf("got %d network events, want 2", len(networkEvents)) + } + + expectedIPs := map[string]uint32{"1.2.3.4": 80, "5.6.7.8": 443} + for _, ne := range networkEvents { + if ne.ProcessKprobe == nil || len(ne.ProcessKprobe.Args) == 0 || ne.ProcessKprobe.Args[0].SockArg == nil { + t.Error("network event missing sock_arg") + continue + } + sock := ne.ProcessKprobe.Args[0].SockArg + wantPort, ok := expectedIPs[sock.DAddr] + if !ok { + t.Errorf("unexpected destination IP: %s", sock.DAddr) + } + if sock.DPort != wantPort { + t.Errorf("for IP %s: dport = %d, want %d", sock.DAddr, sock.DPort, wantPort) + } + } +} + +// --- Tests for observability.go functions --- + +func TestExtractNetworkConnections(t *testing.T) { + eventsData := strings.Join([]string{ + `{"process_exec":{"process":{"binary":"/bin/sh","pid":1}},"time":"2025-01-01T00:00:00Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/wget"},"function_name":"tcp_connect","args":[{"sock_arg":{"family":"AF_INET","protocol":"IPPROTO_TCP","saddr":"10.0.2.15","daddr":"1.2.3.4","sport":54321,"dport":80}}]},"time":"2025-01-01T00:00:01Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/apk"},"function_name":"tcp_connect","args":[{"sock_arg":{"family":"AF_INET","protocol":"IPPROTO_TCP","saddr":"10.0.2.15","daddr":"5.6.7.8","sport":54322,"dport":443}}]},"time":"2025-01-01T00:00:02Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/wget"},"function_name":"tcp_close","args":[{"sock_arg":{"family":"AF_INET","protocol":"IPPROTO_TCP","saddr":"10.0.2.15","daddr":"1.2.3.4","sport":54321,"dport":80}}]},"time":"2025-01-01T00:00:03Z"}`, + `{"process_exit":{"process":{"binary":"/bin/sh","pid":1}},"time":"2025-01-01T00:00:04Z"}`, + }, "\n") + + connections, eventCount := extractNetworkConnections([]byte(eventsData)) + if eventCount != 5 { + t.Errorf("eventCount = %d, want 5", eventCount) + } + if len(connections) != 3 { + t.Errorf("len(connections) = %d, want 3", len(connections)) + } + if len(connections) > 0 { + c := connections[0] + if c.DstAddr != "1.2.3.4" || c.DstPort != 80 { + t.Errorf("connection[0] = %s:%d, want 1.2.3.4:80", c.DstAddr, c.DstPort) + } + if c.Process != "/usr/bin/wget" { + t.Errorf("connection[0].Process = %q, want /usr/bin/wget", c.Process) + } + } +} + +func TestExtractNetworkConnections_EmptyData(t *testing.T) { + connections, eventCount := extractNetworkConnections([]byte("")) + if eventCount != 0 { + t.Errorf("eventCount = %d, want 0", eventCount) + } + if len(connections) != 0 { + t.Errorf("len(connections) = %d, want 0", len(connections)) + } +} + +func TestExtractNetworkConnections_MalformedLines(t *testing.T) { + eventsData := "not json\n{\"process_exec\":{\"process\":{\"binary\":\"/bin/sh\"}}}\n{broken\n" + connections, eventCount := extractNetworkConnections([]byte(eventsData)) + if eventCount != 1 { + t.Errorf("eventCount = %d, want 1 (only the valid line)", eventCount) + } + if len(connections) != 0 { + t.Errorf("len(connections) = %d, want 0 (no network events)", len(connections)) + } +} + +func TestLogObservabilityEvents(t *testing.T) { + ctx := clog.WithLogger(context.Background(), slogtest.TestLogger(t)) + + events := &ObservabilityEvents{ + RawData: []byte(strings.Join([]string{ + `{"process_exec":{"process":{"binary":"/bin/sh","pid":1}},"time":"2025-01-01T00:00:00Z"}`, + `{"process_kprobe":{"process":{"binary":"/usr/bin/wget"},"function_name":"tcp_connect","args":[{"sock_arg":{"daddr":"1.2.3.4","dport":80,"family":"AF_INET","protocol":"IPPROTO_TCP","saddr":"10.0.2.15","sport":54321}}]},"time":"2025-01-01T00:00:01Z"}`, + }, "\n")), + EventCount: 2, + NetworkConnections: []NetworkConnection{ + {Process: "/usr/bin/wget", Protocol: "IPPROTO_TCP", SrcAddr: "10.0.2.15", SrcPort: 54321, DstAddr: "1.2.3.4", DstPort: 80, Family: "AF_INET", Function: "tcp_connect"}, + }, + } + + // Should not panic or error — just logs to the test logger + LogObservabilityEvents(ctx, events) +} + +func TestLogObservabilityEvents_NilEvents(t *testing.T) { + ctx := clog.WithLogger(context.Background(), slogtest.TestLogger(t)) + // Should be a no-op + LogObservabilityEvents(ctx, nil) +} + +func TestLogObservabilityEvents_EmptyRawData(t *testing.T) { + ctx := clog.WithLogger(context.Background(), slogtest.TestLogger(t)) + // Should be a no-op + LogObservabilityEvents(ctx, &ObservabilityEvents{}) +} + +// --- Helper Functions --- + +// parseObservabilityEvents parses NDJSON event data. +// Returns all events and a filtered list of network-related kprobe events. +func parseObservabilityEvents(data []byte) ([]ObservabilityEvent, []ObservabilityEvent, error) { + allEvents := make([]ObservabilityEvent, 0) + var networkEvents []ObservabilityEvent + + for line := range bytes.SplitSeq(data, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + var event ObservabilityEvent + if err := json.Unmarshal(line, &event); err != nil { + return nil, nil, err + } + allEvents = append(allEvents, event) + if event.ProcessKprobe != nil { + fn := event.ProcessKprobe.FunctionName + if fn == "tcp_connect" || fn == "tcp_close" || fn == "tcp_sendmsg" { + networkEvents = append(networkEvents, event) + } + } + } + return allEvents, networkEvents, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/container/qemu_runner.go new/melange-0.48.0/pkg/container/qemu_runner.go --- old/melange-0.47.0/pkg/container/qemu_runner.go 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/pkg/container/qemu_runner.go 2026-03-31 23:10:54.000000000 +0200 @@ -2329,6 +2329,12 @@ if err != nil { return err } + // Skip symlinks — extracted APKs may contain broken absolute symlinks + // that are not kernel modules (e.g. /lib/modules/<ver>/vmlinuz -> /boot/vmlinuz-virt). + // The kernel image is provided separately via QEMU_KERNEL_IMAGE. + if d.Type()&fs.ModeSymlink != 0 { + return nil + } archivePath := filepath.Join(archivePrefix, path) if d.IsDir() { moduleRecords = append(moduleRecords, cpio.Directory(archivePath, 0o755)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/sca/sca.go new/melange-0.48.0/pkg/sca/sca.go --- old/melange-0.47.0/pkg/sca/sca.go 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/pkg/sca/sca.go 2026-03-31 23:10:54.000000000 +0200 @@ -828,6 +828,50 @@ return nil } +// generatePerlDeps generates a perl dependency for packages which ship +// Perl modules. +func generatePerlDeps(ctx context.Context, hdl SCAHandle, generated *config.Dependencies, extraLibDirs []string) error { + log := clog.FromContext(ctx) + log.Infof("scanning for perl modules...") + + fsys, err := hdl.Filesystem() + if err != nil { + return err + } + + perlModuleMatchUsrLib, err := fs.Glob(fsys, "usr/lib/perl[0-9]*/*_perl") + if err != nil { + return err + } + perlModuleMatchLib, err := fs.Glob(fsys, "lib/perl[0-9]*/*_perl") + if err != nil { + return err + } + + perlModuleMatch := slices.Clone(perlModuleMatchUsrLib) + perlModuleMatch = append(perlModuleMatch, perlModuleMatchLib...) + if len(perlModuleMatch) == 0 { + return nil + } + + // Get the 'perlN' component of the path + perlVersion := strings.Split(perlModuleMatch[0], "/")[2] + // Remove the 'perl' prefix + perlVersion, _ = strings.CutPrefix(perlVersion, "perl") + log.Debugf(" perl module depends on Perl version %s", perlVersion) + + // Do not add a Perl dependency if one already exists. + if slices.Contains(hdl.BaseDependencies().Runtime, "perl") { + log.Warnf("%s: Perl dependency 'perl' already specified, consider removing it in favor of SCA-generated dependency", hdl.PackageName()) + return nil + } + + log.Infof(" found perl module, generating perl~%s dependency", perlVersion) + generated.Runtime = append(generated.Runtime, fmt.Sprintf("perl~%s", perlVersion)) + + return nil +} + // generatePythonDeps generates a python-3.X-base dependency for packages which ship // Python modules. func generatePythonDeps(ctx context.Context, hdl SCAHandle, generated *config.Dependencies, extraLibDirs []string) error { @@ -903,10 +947,17 @@ return err } - rubyGemMatches, err := fs.Glob(fsys, "usr/lib/ruby/gems/[0-9]*.[0-9]*.[0.9]*/gems") + rubyGemMatchesUsrLib, err := fs.Glob(fsys, "usr/lib/ruby/gems/[0-9]*.[0-9]*.[0.9]*/gems") if err != nil { return err } + rubyGemMatchesLib, err := fs.Glob(fsys, "lib/ruby/gems/[0-9]*.[0-9]*.[0.9]*/gems") + if err != nil { + return err + } + + rubyGemMatches := slices.Clone(rubyGemMatchesUsrLib) + rubyGemMatches = append(rubyGemMatches, rubyGemMatchesLib...) if len(rubyGemMatches) == 0 { return nil } @@ -1134,6 +1185,7 @@ generateCmdProviders, generateDocDeps, generatePkgConfigDeps, + generatePerlDeps, generatePythonDeps, generateRubyDeps, generateShbangDeps, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/sca/sca_test.go new/melange-0.48.0/pkg/sca/sca_test.go --- old/melange-0.47.0/pkg/sca/sca_test.go 2026-03-27 00:06:38.000000000 +0100 +++ new/melange-0.48.0/pkg/sca/sca_test.go 2026-03-31 23:10:54.000000000 +0200 @@ -217,6 +217,25 @@ } } +func TestPerlSca(t *testing.T) { + ctx := slogtest.Context(t) + // Generated by: + // wget https://packages.wolfi.dev/os/x86_64/perl-json-4.11-r0.apk + th := handleFromApk(ctx, t, "perl-json-4.11-r0.apk", "perl-json.yaml") + defer th.exp.Close() + + got := config.Dependencies{} + if err := Analyze(ctx, th, &got); err != nil { + t.Fatal(err) + } + + want := config.Dependencies{Runtime: []string{"perl~5"}} + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Analyze(): (-want, +got):\n%s", diff) + } +} + func TestRubySca(t *testing.T) { ctx := slogtest.Context(t) // Generated by: Binary files old/melange-0.47.0/pkg/sca/testdata/perl-json-4.11-r0.apk and new/melange-0.48.0/pkg/sca/testdata/perl-json-4.11-r0.apk differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.47.0/pkg/sca/testdata/perl-json.yaml new/melange-0.48.0/pkg/sca/testdata/perl-json.yaml --- old/melange-0.47.0/pkg/sca/testdata/perl-json.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/melange-0.48.0/pkg/sca/testdata/perl-json.yaml 2026-03-31 23:10:54.000000000 +0200 @@ -0,0 +1,91 @@ +package: + name: perl-json + version: "4.11" + epoch: 0 # go/wolfi-rsc/perl-json + description: Perl module implementing a JSON encoder/decoder + copyright: + - license: GPL-1.0-or-later OR Artistic-1.0-Perl + resources: + memory: 1Gi + +environment: + contents: + packages: + - autoconf + - automake + - build-base + - busybox + - ca-certificates-bundle + - perl + - perl-dev + +pipeline: + - uses: git-checkout + with: + repository: https://github.com/makamaka/JSON + tag: ${{package.version}} + expected-commit: 2e2812bb38a143ef2e0a553104c0a58a693f8ca7 + + - runs: PERL_MM_USE_DEFAULT=1 perl Makefile.PL INSTALLDIRS=vendor + + - uses: autoconf/make + + - uses: autoconf/make-install + + - uses: strip + +subpackages: + - name: perl-json-doc + pipeline: + - uses: split/manpages + description: perl-json manpages + test: + pipeline: + - uses: test/docs + +test: + environment: + contents: + packages: + - perl + pipeline: + - name: Verify module loads + runs: perl -e 'use JSON; print "JSON loaded\n"' + - name: Test JSON encoding + runs: | + perl -e ' + use JSON; + my $json = JSON->new->allow_nonref; + my $encoded = $json->encode({key => "value", num => 42}); + die "encode failed" unless defined $encoded; + print "encode ok: $encoded\n"; + ' + - name: Test JSON decoding + runs: | + perl -e ' + use JSON; + my $json = JSON->new->allow_nonref; + my $decoded = $json->decode('"'"'{"key":"value","num":42}'"'"'); + die "decode failed" unless ref($decoded) eq "HASH"; + die "key mismatch" unless $decoded->{key} eq "value"; + die "num mismatch" unless $decoded->{num} == 42; + print "decode ok\n"; + ' + - name: Test encode/decode roundtrip + runs: | + perl -e ' + use JSON; + my $data = {list => [1, 2, 3], nested => {a => "b"}}; + my $encoded = encode_json($data); + my $decoded = decode_json($encoded); + die "roundtrip failed for list" unless ref($decoded->{list}) eq "ARRAY"; + die "roundtrip failed for nested" unless $decoded->{nested}{a} eq "b"; + print "roundtrip ok\n"; + ' + - uses: test/tw/ldd-check + +update: + enabled: true + github: + identifier: makamaka/JSON + use-tag: true ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.phWb2k/_old 2026-04-02 17:43:58.538320089 +0200 +++ /var/tmp/diff_new_pack.phWb2k/_new 2026-04-02 17:43:58.542320254 +0200 @@ -1,5 +1,5 @@ name: melange -version: 0.47.0 -mtime: 1774566398 -commit: 3fa79327ebf36fb4b3700d6a053b95836e45ccd8 +version: 0.48.0 +mtime: 1774991454 +commit: 54faa684108e9d1e0a716adc3819e105da614bf7 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.21863/vendor.tar.gz differ: char 133, line 1
