Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6dd0035e authored by Bob Badour's avatar Bob Badour
Browse files

compliance package: listshare and checkshare

package to read, consume, and analyze license metadata and dependency
graph.

Includes the below command-line tools:

listshare outputs csv of projects to share to meet restricted and
reciprocal license requirements with one project per line. The first
field is the path to the project, and subsequent fields identify the
license resolutions as colon-separated target:annotations tuples.

checkshare outputs error messages to stderr for any targets where
policy dictates both sharing and not sharing the source-code, and PASS
or FAIL to stdout. exit status indicates success 0 or conflict found 1

Bug: 68860345
Bug: 151177513
Bug: 151953481

Test: m all
Test: m systemlicense
Test: m listshare; out/soong/host/linux-x86/bin/listshare ...
Test: m checkshare; out/soong/host/linux-x86/bin/checkshare ...
Test: m dumpgraph; out/soong/host/linux-x86/dumpgraph ...
Test: m dumpresolutions; out/soong/host/linux-x86/dumpresolutions ...

where ... is the path to the .meta_lic file for the system image. In my
case if

$ export PRODUCT=$(realpath $ANDROID_PRODUCT_OUT --relative-to=$PWD)

... can be expressed as:

${PRODUCT}/gen/META/lic_intermediates/${PRODUCT}/system.img.meta_lic

Change-Id: I4ff3f98848f7e6d03a35734300d763ef5f245d53
parent 1ded0a1b
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -17,6 +17,20 @@ package {
    default_applicable_licenses: ["Android-Apache-2.0"],
}

blueprint_go_binary {
    name: "checkshare",
    srcs: ["cmd/checkshare.go"],
    deps: ["compliance-module"],
    testSrcs: ["cmd/checkshare_test.go"],
}

blueprint_go_binary {
    name: "listshare",
    srcs: ["cmd/listshare.go"],
    deps: ["compliance-module"],
    testSrcs: ["cmd/listshare_test.go"],
}

blueprint_go_binary {
    name: "dumpgraph",
    srcs: ["cmd/dumpgraph.go"],
+114 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// 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 main

import (
	"compliance"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
)

func init() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, `Usage: %s file.meta_lic {file.meta_lic...}

Reports on stderr any targets where policy says that the source both
must and must not be shared. The error report indicates the target, the
license condition with origin that has a source privacy policy, and the
license condition with origin that has a source sharing policy.

Any given target may appear multiple times with different combinations
of conflicting license conditions.

If all the source code that policy says must be shared may be shared,
outputs "PASS" to stdout and exits with status 0.

If policy says any source must both be shared and not be shared,
outputs "FAIL" to stdout and exits with status 1.
`, filepath.Base(os.Args[0]))
	}
}

var (
	failConflicts = fmt.Errorf("conflicts")
	failNoneRequested = fmt.Errorf("\nNo metadata files requested")
	failNoLicenses = fmt.Errorf("No licenses")
)


// byError orders conflicts by error string
type byError []compliance.SourceSharePrivacyConflict

func (l byError) Len() int           { return len(l) }
func (l byError) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
func (l byError) Less(i, j int) bool { return l[i].Error() < l[j].Error() }

func main() {
	flag.Parse()

	// Must specify at least one root target.
	if flag.NArg() == 0 {
		flag.Usage()
		os.Exit(2)
	}

	err := checkShare(os.Stdout, os.Stderr, flag.Args()...)
	if err != nil {
		if err != failConflicts {
			if err == failNoneRequested {
				flag.Usage()
			}
			fmt.Fprintf(os.Stderr, "%s\n", err.Error())
		}
		os.Exit(1)
	}
	os.Exit(0)
}

// checkShare implements the checkshare utility.
func checkShare(stdout, stderr io.Writer, files ...string) error {

	if len(files) < 1 {
		return failNoneRequested
	}

	// Read the license graph from the license metadata files (*.meta_lic).
	licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
	if err != nil {
		return fmt.Errorf("Unable to read license metadata file(s) %q: %w\n", files, err)
	}
	if licenseGraph == nil {
		return failNoLicenses
	}

	// Apply policy to find conflicts and report them to stderr lexicographically ordered.
	conflicts := compliance.ConflictingSharedPrivateSource(licenseGraph)
	sort.Sort(byError(conflicts))
	for _, conflict := range conflicts {
		fmt.Fprintln(stderr, conflict.Error())
	}

	// Indicate pass or fail on stdout.
	if len(conflicts) > 0 {
		fmt.Fprintln(stdout, "FAIL")
		return failConflicts
	}
	fmt.Fprintln(stdout, "PASS")
	return nil
}
+299 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// 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 main

import (
	"bytes"
	"fmt"
	"strings"
	"testing"
)

type outcome struct {
	target           string
	privacyOrigin    string
	privacyCondition string
	shareOrigin      string
	shareCondition   string
}

func (o *outcome) String() string {
	return fmt.Sprintf("%s %s from %s and must share from %s %s",
		o.target, o.privacyCondition, o.privacyOrigin, o.shareCondition, o.shareOrigin)
}

type outcomeList []*outcome

func (ol outcomeList) String() string {
	result := ""
	for _, o := range ol {
		result = result + o.String() + "\n"
	}
	return result
}

func Test(t *testing.T) {
	tests := []struct {
		condition        string
		name             string
		roots            []string
		expectedStdout   string
		expectedOutcomes outcomeList
	}{
		{
			condition:      "firstparty",
			name:           "apex",
			roots:          []string{"highest.apex.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "firstparty",
			name:           "container",
			roots:          []string{"container.zip.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "firstparty",
			name:           "application",
			roots:          []string{"application.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "firstparty",
			name:           "binary",
			roots:          []string{"bin/bin2.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "firstparty",
			name:           "library",
			roots:          []string{"lib/libd.so.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "notice",
			name:           "apex",
			roots:          []string{"highest.apex.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "notice",
			name:           "container",
			roots:          []string{"container.zip.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "notice",
			name:           "application",
			roots:          []string{"application.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "notice",
			name:           "binary",
			roots:          []string{"bin/bin2.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "notice",
			name:           "library",
			roots:          []string{"lib/libd.so.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "reciprocal",
			name:           "apex",
			roots:          []string{"highest.apex.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "reciprocal",
			name:           "container",
			roots:          []string{"container.zip.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "reciprocal",
			name:           "application",
			roots:          []string{"application.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "reciprocal",
			name:           "binary",
			roots:          []string{"bin/bin2.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "reciprocal",
			name:           "library",
			roots:          []string{"lib/libd.so.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "restricted",
			name:           "apex",
			roots:          []string{"highest.apex.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "restricted",
			name:           "container",
			roots:          []string{"container.zip.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "restricted",
			name:           "application",
			roots:          []string{"application.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "restricted",
			name:           "binary",
			roots:          []string{"bin/bin2.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "restricted",
			name:           "library",
			roots:          []string{"lib/libd.so.meta_lic"},
			expectedStdout: "PASS",
		},
		{
			condition:      "proprietary",
			name:           "apex",
			roots:          []string{"highest.apex.meta_lic"},
			expectedStdout: "FAIL",
			expectedOutcomes: outcomeList{
				&outcome{
					target:           "testdata/proprietary/bin/bin2.meta_lic",
					privacyOrigin:    "testdata/proprietary/bin/bin2.meta_lic",
					privacyCondition: "proprietary",
					shareOrigin:      "testdata/proprietary/lib/libb.so.meta_lic",
					shareCondition:   "restricted",
				},
			},
		},
		{
			condition:      "proprietary",
			name:           "container",
			roots:          []string{"container.zip.meta_lic"},
			expectedStdout: "FAIL",
			expectedOutcomes: outcomeList{
				&outcome{
					target:           "testdata/proprietary/bin/bin2.meta_lic",
					privacyOrigin:    "testdata/proprietary/bin/bin2.meta_lic",
					privacyCondition: "proprietary",
					shareOrigin:      "testdata/proprietary/lib/libb.so.meta_lic",
					shareCondition:   "restricted",
				},
			},
		},
		{
			condition:      "proprietary",
			name:           "application",
			roots:          []string{"application.meta_lic"},
			expectedStdout: "FAIL",
			expectedOutcomes: outcomeList{
				&outcome{
					target:           "testdata/proprietary/lib/liba.so.meta_lic",
					privacyOrigin:    "testdata/proprietary/lib/liba.so.meta_lic",
					privacyCondition: "proprietary",
					shareOrigin:      "testdata/proprietary/lib/libb.so.meta_lic",
					shareCondition:   "restricted",
				},
			},
		},
		{
			condition:      "proprietary",
			name:           "binary",
			roots:          []string{"bin/bin2.meta_lic", "lib/libb.so.meta_lic"},
			expectedStdout: "FAIL",
			expectedOutcomes: outcomeList{
				&outcome{
					target:           "testdata/proprietary/bin/bin2.meta_lic",
					privacyOrigin:    "testdata/proprietary/bin/bin2.meta_lic",
					privacyCondition: "proprietary",
					shareOrigin:      "testdata/proprietary/lib/libb.so.meta_lic",
					shareCondition:   "restricted",
				},
			},
		},
		{
			condition:      "proprietary",
			name:           "library",
			roots:          []string{"lib/libd.so.meta_lic"},
			expectedStdout: "PASS",
		},
	}
	for _, tt := range tests {
		t.Run(tt.condition+" "+tt.name, func(t *testing.T) {
			stdout := &bytes.Buffer{}
			stderr := &bytes.Buffer{}

			rootFiles := make([]string, 0, len(tt.roots))
			for _, r := range tt.roots {
				rootFiles = append(rootFiles, "testdata/"+tt.condition+"/"+r)
			}
			err := checkShare(stdout, stderr, rootFiles...)
			if err != nil && err != failConflicts {
				t.Fatalf("checkshare: error = %v, stderr = %v", err, stderr)
				return
			}
			var actualStdout string
			for _, s := range strings.Split(stdout.String(), "\n") {
				ts := strings.TrimLeft(s, " \t")
				if len(ts) < 1 {
					continue
				}
				if 0 < len(actualStdout) {
					t.Errorf("checkshare: unexpected multiple output lines %q, want %q", actualStdout+"\n"+ts, tt.expectedStdout)
				}
				actualStdout = ts
			}
			if actualStdout != tt.expectedStdout {
				t.Errorf("checkshare: unexpected stdout %q, want %q", actualStdout, tt.expectedStdout)
			}
			errList := strings.Split(stderr.String(), "\n")
			actualOutcomes := make(outcomeList, 0, len(errList))
			for _, cstring := range errList {
				ts := strings.TrimLeft(cstring, " \t")
				if len(ts) < 1 {
					continue
				}
				cFields := strings.Split(ts, " ")
				actualOutcomes = append(actualOutcomes, &outcome{
					target:           cFields[0],
					privacyOrigin:    cFields[3],
					privacyCondition: cFields[1],
					shareOrigin:      cFields[9],
					shareCondition:   cFields[8],
				})
			}
			if len(actualOutcomes) != len(tt.expectedOutcomes) {
				t.Errorf("checkshare: unexpected got %d outcomes %s, want %d outcomes %s",
					len(actualOutcomes), actualOutcomes, len(tt.expectedOutcomes), tt.expectedOutcomes)
				return
			}
			for i := range actualOutcomes {
				if actualOutcomes[i].String() != tt.expectedOutcomes[i].String() {
					t.Errorf("checkshare: unexpected outcome #%d, got %q, want %q",
						i+1, actualOutcomes[i], tt.expectedOutcomes[i])
				}
			}
		})
	}
}
+124 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// 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 main

import (
	"compliance"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
)

func init() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, `Usage: %s file.meta_lic {file.meta_lic...}

Outputs a csv file with 1 project per line in the first field followed
by target:condition pairs describing why the project must be shared.

Each target is the path to a generated license metadata file for a
Soong module or Make target, and the license condition is either
restricted (e.g. GPL) or reciprocal (e.g. MPL).
`, filepath.Base(os.Args[0]))
	}
}

var (
	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
	failNoLicenses = fmt.Errorf("No licenses found")
)

func main() {
	flag.Parse()

	// Must specify at least one root target.
	if flag.NArg() == 0 {
		flag.Usage()
		os.Exit(2)
	}

	err := listShare(os.Stdout, os.Stderr, flag.Args()...)
	if err != nil {
		if err == failNoneRequested {
			flag.Usage()
		}
		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
		os.Exit(1)
	}
	os.Exit(0)
}

// listShare implements the listshare utility.
func listShare(stdout, stderr io.Writer, files ...string) error {
	// Must be at least one root file.
	if len(files) < 1 {
		return failNoneRequested
	}

	// Read the license graph from the license metadata files (*.meta_lic).
	licenseGraph, err := compliance.ReadLicenseGraph(os.DirFS("."), stderr, files)
	if err != nil {
		return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
	}
	if licenseGraph == nil {
		return failNoLicenses
	}

	// shareSource contains all source-sharing resolutions.
	shareSource := compliance.ResolveSourceSharing(licenseGraph)

	// Group the resolutions by project.
	presolution := make(map[string]*compliance.LicenseConditionSet)
	for _, target := range shareSource.AttachesTo() {
		rl := shareSource.Resolutions(target)
		sort.Sort(rl)
		for _, r := range rl {
			for _, p := range r.ActsOn().Projects() {
				if _, ok := presolution[p]; !ok {
					presolution[p] = r.Resolves().Copy()
					continue
				}
				presolution[p].AddSet(r.Resolves())
			}
		}
	}

	// Sort the projects for repeatability/stability.
	projects := make([]string, 0, len(presolution))
	for p := range presolution {
		projects = append(projects, p)
	}
	sort.Strings(projects)

	// Output the sorted projects and the source-sharing license conditions that each project resolves.
	for _, p := range projects {
		fmt.Fprintf(stdout, "%s", p)

		// Sort the conditions for repeatability/stability.
		conditions := presolution[p].AsList()
		sort.Sort(conditions)

		// Output the sorted origin:condition pairs.
		for _, lc := range conditions {
			fmt.Fprintf(stdout, ",%s:%s", lc.Origin().Name(), lc.Name())
		}
		fmt.Fprintf(stdout, "\n")
	}

	return nil
}
+405 −0

File added.

Preview size limit exceeded, changes collapsed.