Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/md/melange_license-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ melange license-check file [flags]

```
--fix fix license issues in the melange yaml file
--format string license fix strategy format: 'simple' or 'flat' (default "flat")
-h, --help help for license-check
--workdir string path to the working directory, e.g. where the source will be extracted to
```
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/license_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
func licenseCheck() *cobra.Command {
var workDir string
var fix bool
var format string
cmd := &cobra.Command{
Use: "license-check file",
Short: "Gather and check licensing data",
Expand Down Expand Up @@ -81,6 +82,7 @@ func licenseCheck() *cobra.Command {
ctx,
copyright.WithLicenses(detectedLicenses),
copyright.WithDiffs(diffs),
copyright.WithFormat(format),
)
err = rc.Renovate(cmd.Context(), copyrightRenovator)
}
Expand All @@ -91,6 +93,7 @@ func licenseCheck() *cobra.Command {

cmd.Flags().StringVar(&workDir, "workdir", "", "path to the working directory, e.g. where the source will be extracted to")
cmd.Flags().BoolVar(&fix, "fix", false, "fix license issues in the melange yaml file")
cmd.Flags().StringVar(&format, "format", "flat", "license fix strategy format: 'simple' or 'flat'")

return cmd
}
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ func (p Package) LicenseExpression() string {
}
for _, cp := range p.Copyright {
if licenseExpression != "" {
licenseExpression += " OR "
licenseExpression += " AND "
}
licenseExpression += cp.License
}
Expand Down
136 changes: 101 additions & 35 deletions pkg/renovate/copyright/copyright.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ package copyright
import (
"context"
"fmt"
"maps"
"slices"
"strings"

"github.com/chainguard-dev/clog"

Expand All @@ -30,6 +33,7 @@ import (
type CopyrightConfig struct {
Licenses []license.License
Diffs []license.LicenseDiff
Format string // "simple" or "flat"
}

// Option sets a config option on a CopyrightConfig.
Expand All @@ -53,6 +57,16 @@ func WithDiffs(diffs []license.LicenseDiff) Option {
}
}

// WithFormat sets whether the copyright should be populated with a single
// node containing all detected licenses joined together (simple), or with
// multiple nodes, one per license (flat).
func WithFormat(format string) Option {
return func(cfg *CopyrightConfig) error {
cfg.Format = format
return nil
}
}

// New returns a renovator which performs a copyright update.
func New(ctx context.Context, opts ...Option) renovate.Renovator {
log := clog.FromContext(ctx)
Expand Down Expand Up @@ -104,46 +118,98 @@ func New(ctx context.Context, opts ...Option) renovate.Renovator {
copyrightNode.Content = nil

// Repopulate the copyrightNode with detected licenses
for _, l := range ccfg.Licenses {
// Skip licenses we don't have full confidence in.
if !license.IsLicenseMatchConfident(l) {
log.Infof("skipping unconfident license %s", l.Source)
continue
if ccfg.Format == "simple" {
// Make the copyright field a single node with all licenses joined together
if err = populateSimpleCopyright(ctx, copyrightNode, ccfg.Licenses); err != nil {
return err
}

licenseNode := &yaml.Node{
Kind: yaml.MappingNode,
Style: yaml.FlowStyle,
Content: []*yaml.Node{},
} else {
// Use flat license listing (original behavior)
if err = populateFlatCopyright(ctx, copyrightNode, ccfg.Licenses); err != nil {
return err
}
}

return nil
}
}

// populateFlatCopyright populates the copyright node with the detected licenses,
// one entry per license.
func populateFlatCopyright(ctx context.Context, copyrightNode *yaml.Node, licenses []license.License) error {
log := clog.FromContext(ctx)

licenseNode.Content = append(licenseNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "license",
Tag: "!!str",
Style: yaml.FlowStyle,
}, &yaml.Node{
Kind: yaml.ScalarNode,
Value: l.Name,
Tag: "!!str",
Style: yaml.FlowStyle,
})

licenseNode.Content = append(licenseNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "license-path",
Tag: "!!str",
Style: yaml.FlowStyle,
}, &yaml.Node{
Kind: yaml.ScalarNode,
Value: l.Source,
Tag: "!!str",
Style: yaml.FlowStyle,
})

copyrightNode.Content = append(copyrightNode.Content, licenseNode)
for _, l := range licenses {
// Skip licenses we don't have full confidence in.
if !license.IsLicenseMatchConfident(l) {
log.Infof("skipping unconfident license %s", l.Source)
continue
}

licenseNode := &yaml.Node{
Kind: yaml.MappingNode,
Style: yaml.FlowStyle,
Content: []*yaml.Node{},
}

licenseNode.Content = append(licenseNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "license",
Tag: "!!str",
Style: yaml.FlowStyle,
}, &yaml.Node{
Kind: yaml.ScalarNode,
Value: l.Name,
Tag: "!!str",
Style: yaml.FlowStyle,
})

licenseNode.Content = append(licenseNode.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: "license-path",
Tag: "!!str",
Style: yaml.FlowStyle,
}, &yaml.Node{
Kind: yaml.ScalarNode,
Value: l.Source,
Tag: "!!str",
Style: yaml.FlowStyle,
})

copyrightNode.Content = append(copyrightNode.Content, licenseNode)
}

return nil
}

// populateSimpleCopyright populates the copyright field with a single node
// with all detected licenses joined together.
func populateSimpleCopyright(ctx context.Context, copyrightNode *yaml.Node, licenses []license.License) error {
log := clog.FromContext(ctx)

// Gather all the license names and concatenate them with AND statements
licenseMap := make(map[string]struct{})
for _, l := range licenses {
if !license.IsLicenseMatchConfident(l) {
log.Infof("skipping unconfident license %s", l.Source)
continue
}
licenseMap[l.Name] = struct{}{}
}

if len(licenseMap) == 0 {
log.Infof("no confident licenses found to populate copyright")
return nil
}

// Join the license names with " AND ", sorting them first for consistency
ls := slices.Collect(maps.Keys(licenseMap))
slices.Sort(ls)
combined := strings.Join(ls, " AND ")

copyrightNode.Kind = yaml.ScalarNode
copyrightNode.Value = combined
copyrightNode.Tag = "!!str"

return nil
}
63 changes: 63 additions & 0 deletions pkg/renovate/copyright/copyright_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,66 @@ func TestCopyright_noDiffs(t *testing.T) {
assert.Contains(t, string(resultData), "license: Not-Applicable")
assert.NotContains(t, string(resultData), "license: Invalid")
}

func TestCopyright_updateSimple(t *testing.T) {
dir := t.TempDir()
ctx := slogtest.Context(t)

detectedLicenses := []license.License{
{
Name: "Apache-2.0",
Source: "LICENSE",
Confidence: 1.0,
},
{
Name: "MIT",
Source: "internal/COPYING",
Confidence: 1.0,
},
{
Name: "Apache-2.0",
Source: "vendor/foo/LICENSE",
Confidence: 1.0,
},
{
Name: "GPL-3.0",
Source: "internal/LICENSE",
Confidence: 0.2,
},
}

diffs := []license.LicenseDiff{
{
Path: "LICENSE",
Is: "GPL-2.0",
Should: "Apache-2.0",
},
}

// Copy the test data file to the temp directory
src := filepath.Join("testdata", "nolicense.yaml")
testFile := filepath.Join(dir, "nolicense.yaml")
input, err := os.ReadFile(src)
assert.NoError(t, err)

err = os.WriteFile(testFile, input, 0644)
assert.NoError(t, err)

rctx, err := renovate.New(renovate.WithConfig(testFile))
assert.NoError(t, err)

copyrightRenovator := New(ctx, WithLicenses(detectedLicenses), WithDiffs(diffs), WithFormat("simple"))

err = rctx.Renovate(slogtest.Context(t), copyrightRenovator)
assert.NoError(t, err)

resultData, err := os.ReadFile(testFile)
assert.NoError(t, err)

// The copyright field should be a single string with both licenses joined by " AND "
result := string(resultData)
assert.Contains(t, result, "Apache-2.0 AND MIT")
assert.NotContains(t, result, "GPL-3.0")
assert.NotContains(t, result, "NOASSERTION")
assert.NotContains(t, result, "license:")
}
Loading