Skip to content

Commit 34203ee

Browse files
feat: allow subpackage copyright metadata
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
1 parent 9b69525 commit 34203ee

9 files changed

Lines changed: 145 additions & 7 deletions

File tree

docs/BUILD-FILE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ The following are the high level sections for the build file, with detailed desc
2323
### subpackages
2424

2525
List of subpackages that this package also produces. For example, docs.
26+
Subpackages may define their own `copyright` entries when their files have
27+
different licensing from the origin package.
2628

2729
### data
2830

@@ -499,4 +501,3 @@ TODO(vaikas): melange config points to apko here:
499501

500502
# pipeline
501503
Pipeline defines the ordered steps to build the package.
502-

pkg/build/package.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type PackageBuild struct {
7373
Description string
7474
URL string
7575
Commit string
76+
Copyright []config.Copyright
7677
}
7778

7879
func pkgFromSub(sub *config.Subpackage) *config.Package {
@@ -84,6 +85,7 @@ func pkgFromSub(sub *config.Subpackage) *config.Package {
8485
Description: sub.Description,
8586
URL: sub.URL,
8687
Commit: sub.Commit,
88+
Copyright: sub.Copyright,
8789
}
8890
}
8991

@@ -102,6 +104,10 @@ func (b *Build) Emit(ctx context.Context, pkg *config.Package) error {
102104
Description: pkg.Description,
103105
URL: pkg.URL,
104106
Commit: pkg.Commit,
107+
Copyright: pkg.Copyright,
108+
}
109+
if len(pc.Copyright) == 0 {
110+
pc.Copyright = pc.Origin.Copyright
105111
}
106112

107113
if !b.StripOriginName {
@@ -166,7 +172,7 @@ maintainer = {{.Build.Namespace}}
166172
{{- if ne .Build.SourceDateEpoch.Unix 0 }}
167173
builddate = {{ .Build.SourceDateEpoch.Unix }}
168174
{{- end}}
169-
{{- range $copyright := .Origin.Copyright }}
175+
{{- range $copyright := .Copyright }}
170176
license = {{ $copyright.License }}
171177
{{- end }}
172178
{{- range $dep := .Dependencies.Runtime }}

pkg/build/package_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ func Test_GenerateControlData(t *testing.T) {
8080
Description: "I'm a unit test",
8181
URL: "https://chainguard.dev",
8282
Commit: "deadbeef",
83-
DataHash: "baadf00d",
83+
Copyright: []config.Copyright{
84+
{License: "Apache-2.0"},
85+
},
86+
DataHash: "baadf00d",
8487
},
8588
want: `# Generated by melange
8689
pkgname = glibc
@@ -92,6 +95,7 @@ pkgdesc = I'm a unit test
9295
url = https://chainguard.dev
9396
commit = deadbeef
9497
maintainer = wolfi
98+
license = Apache-2.0
9599
datahash = baadf00d
96100
`,
97101
}, {
@@ -138,3 +142,16 @@ datahash = baadf00d
138142
})
139143
}
140144
}
145+
146+
func Test_pkgFromSub_Copyright(t *testing.T) {
147+
sub := &config.Subpackage{
148+
Name: "glibc-dev",
149+
Copyright: []config.Copyright{
150+
{License: "LGPL-2.1-or-later"},
151+
},
152+
}
153+
154+
pkg := pkgFromSub(sub)
155+
156+
require.Equal(t, sub.Copyright, pkg.Copyright)
157+
}

pkg/build/sbom/spdx/spdx.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/spdx/tools-golang/spdx/v2/common"
3030

3131
build "chainguard.dev/melange/pkg/build/sbom"
32+
"chainguard.dev/melange/pkg/config"
3233
"chainguard.dev/melange/pkg/sbom"
3334
)
3435

@@ -121,13 +122,14 @@ func (g *Generator) GenerateSPDX(ctx context.Context, gc *build.GeneratorContext
121122
// Add APK packages to their respective SBOMs
122123
for _, sp := range gc.Configuration.Subpackages {
123124
spSBOM := sg.Document(sp.Name)
125+
spPkg := packageForSubpackage(pkg, &sp)
124126

125127
apkSubPkg := &sbom.Package{
126128
IDComponents: []string{"apk", sp.Name, pkg.FullVersion()},
127129
Name: sp.Name,
128130
Version: pkg.FullVersion(),
129-
Copyright: pkg.FullCopyright(),
130-
LicenseDeclared: pkg.LicenseExpression(),
131+
Copyright: spPkg.FullCopyright(),
132+
LicenseDeclared: spPkg.LicenseExpression(),
131133
Namespace: gc.Namespace,
132134
Arch: arch,
133135
PURL: pkg.PackageURLForSubpackage(gc.Namespace, arch, sp.Name),
@@ -139,7 +141,7 @@ func (g *Generator) GenerateSPDX(ctx context.Context, gc *build.GeneratorContext
139141
// Add upstream source packages from subpackage pipelines
140142
for i, p := range sp.Pipeline {
141143
uniqueID := strconv.Itoa(i)
142-
upstreamPkg, err := p.SBOMPackageForUpstreamSource(pkg.LicenseExpression(), gc.Namespace, uniqueID)
144+
upstreamPkg, err := p.SBOMPackageForUpstreamSource(spPkg.LicenseExpression(), gc.Namespace, uniqueID)
143145
if err != nil {
144146
return nil, fmt.Errorf("creating SBOM package for upstream source in subpackage %s: %w", sp.Name, err)
145147
}
@@ -206,7 +208,7 @@ func (g *Generator) GenerateSPDX(ctx context.Context, gc *build.GeneratorContext
206208
}
207209

208210
// Add licensing information
209-
li, err := gc.Configuration.Package.LicensingInfos(ctx, gc.WorkspaceDir)
211+
li, err := licensingInfos(ctx, gc)
210212
if err != nil {
211213
return nil, fmt.Errorf("gathering licensing infos: %w", err)
212214
}
@@ -227,6 +229,40 @@ func (g *Generator) GenerateSPDX(ctx context.Context, gc *build.GeneratorContext
227229
return out, nil
228230
}
229231

232+
func packageForSubpackage(pkg *config.Package, sp *config.Subpackage) config.Package {
233+
copyright := sp.Copyright
234+
if len(copyright) == 0 {
235+
copyright = pkg.Copyright
236+
}
237+
238+
return config.Package{
239+
Name: sp.Name,
240+
Version: pkg.Version,
241+
Epoch: pkg.Epoch,
242+
Copyright: copyright,
243+
}
244+
}
245+
246+
func licensingInfos(ctx context.Context, gc *build.GeneratorContext) (map[string]string, error) {
247+
out, err := gc.Configuration.Package.LicensingInfos(ctx, gc.WorkspaceDir)
248+
if err != nil {
249+
return nil, err
250+
}
251+
252+
for _, sp := range gc.Configuration.Subpackages {
253+
spPkg := packageForSubpackage(&gc.Configuration.Package, &sp)
254+
li, err := spPkg.LicensingInfos(ctx, gc.WorkspaceDir)
255+
if err != nil {
256+
return nil, err
257+
}
258+
for k, v := range li {
259+
out[k] = v
260+
}
261+
}
262+
263+
return out, nil
264+
}
265+
230266
// GenerateSBOM generates and writes SPDX SBOM documents for the main package and
231267
// all subpackages based on the build context.
232268
func (g *Generator) GenerateSBOM(ctx context.Context, gc *build.GeneratorContext) error {

pkg/build/sbom/spdx/spdx_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,68 @@ func TestSBOMGeneration(t *testing.T) {
260260
}
261261
}
262262

263+
func TestSBOMGenerationUsesSubpackageCopyright(t *testing.T) {
264+
tmpDir := t.TempDir()
265+
ctx := context.Background()
266+
outputFS := apkofs.DirFS(ctx, tmpDir)
267+
268+
cfg := &config.Configuration{
269+
Package: config.Package{
270+
Name: "test-pkg",
271+
Version: "1.2.3",
272+
Epoch: 2,
273+
Copyright: []config.Copyright{
274+
{License: "MIT"},
275+
},
276+
},
277+
Subpackages: []config.Subpackage{
278+
{
279+
Name: "test-pkg-dev",
280+
Copyright: []config.Copyright{
281+
{
282+
License: "Apache-2.0",
283+
Attestation: "Copyright 2026 Example",
284+
},
285+
},
286+
},
287+
},
288+
}
289+
290+
genCtx := &build.GeneratorContext{
291+
Configuration: cfg,
292+
WorkspaceDir: tmpDir,
293+
OutputFS: outputFS,
294+
SourceDateEpoch: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
295+
Namespace: "test-ns",
296+
Arch: "x86_64",
297+
}
298+
299+
gen := &Generator{}
300+
sboms, err := gen.GenerateSPDX(ctx, genCtx)
301+
if err != nil {
302+
t.Fatalf("GenerateSPDX failed: %v", err)
303+
}
304+
305+
subDoc := sboms["test-pkg-dev"]
306+
var subPackage *spdx.Package
307+
for i := range subDoc.Packages {
308+
if subDoc.Packages[i].Name == "test-pkg-dev" {
309+
subPackage = &subDoc.Packages[i]
310+
break
311+
}
312+
}
313+
if subPackage == nil {
314+
t.Fatal("subpackage package not found in SBOM")
315+
}
316+
317+
if diff := cmp.Diff("Apache-2.0", subPackage.LicenseDeclared); diff != "" {
318+
t.Errorf("subpackage license declared mismatch (-want +got):\n%s", diff)
319+
}
320+
if diff := cmp.Diff("Copyright 2026 Example\n", subPackage.CopyrightText); diff != "" {
321+
t.Errorf("subpackage copyright mismatch (-want +got):\n%s", diff)
322+
}
323+
}
324+
263325
func TestSBOMGenerationWithNonSPDXLicense(t *testing.T) {
264326
tmpDir := t.TempDir()
265327
ctx := context.Background()

pkg/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,8 @@ type Subpackage struct {
861861
URL string `json:"url,omitempty" yaml:"url,omitempty"`
862862
// Optional: The git commit of the subpackage build configuration
863863
Commit string `json:"commit,omitempty" yaml:"commit,omitempty"`
864+
// Optional: The list of copyrights for this subpackage
865+
Copyright []Copyright `json:"copyright,omitempty" yaml:"copyright,omitempty"`
864866
// Optional: enabling, disabling, and configuration of build checks
865867
Checks Checks `json:"checks" yaml:"checks,omitempty"`
866868
// Test section for the subpackage.
@@ -1560,6 +1562,7 @@ func replaceSubpackage(r *strings.Replacer, detectedCommit string, in Subpackage
15601562
Description: r.Replace(in.Description),
15611563
URL: r.Replace(in.URL),
15621564
Commit: replaceCommit(detectedCommit, in.Commit),
1565+
Copyright: in.Copyright,
15631566
Checks: in.Checks,
15641567
Test: replaceTest(r, in.Test),
15651568
}

pkg/config/config_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ var-transforms:
107107
108108
subpackages:
109109
- name: subpackage-${{vars.short-package-version}}
110+
copyright:
111+
- license: Apache-2.0
110112
dependencies:
111113
runtime:
112114
- ${{package.name}}-config-${{package.version}}
@@ -181,6 +183,7 @@ test:
181183
}, cfg.Test.Environment.Contents.Packages)
182184

183185
require.Equal(t, cfg.Subpackages[0].Name, "subpackage-0.0")
186+
require.Equal(t, []Copyright{{License: "Apache-2.0"}}, cfg.Subpackages[0].Copyright)
184187

185188
require.Equal(t, "/usr/local/FOO", cfg.Test.Environment.Environment["LD_LIBRARY_PATH"])
186189
}

pkg/config/schema.cue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,9 @@
567567
// Optional: The git commit of the subpackage build configuration
568568
commit?: string
569569

570+
// Optional: The list of copyrights for this subpackage
571+
copyright?: [...#Copyright]
572+
570573
// Optional: enabling, disabling, and configuration of build
571574
// checks
572575
checks!: #Checks

pkg/config/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,13 @@
10331033
"type": "string",
10341034
"description": "Optional: The git commit of the subpackage build configuration"
10351035
},
1036+
"copyright": {
1037+
"items": {
1038+
"$ref": "#/$defs/Copyright"
1039+
},
1040+
"type": "array",
1041+
"description": "Optional: The list of copyrights for this subpackage"
1042+
},
10361043
"checks": {
10371044
"$ref": "#/$defs/Checks",
10381045
"description": "Optional: enabling, disabling, and configuration of build checks"

0 commit comments

Comments
 (0)