Repository: bpicode/fritzctl
Pull request #153 (2018-04-21 16:45:25)

Tool: deplock2notice, auto-generate NOTICE file

codecov[bot]: # [Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=h1) Report
> Merging [#153](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=desc) into [master](https://codecov.io/gh/bpicode/fritzctl/commit/023b457533328468b910a9e9afcca7c6e33c0e18?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/bpicode/fritzctl/pull/153/graphs/tree.svg?height=150&width=650&token=BE6YV2nOnc&src=pr)](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #153    +/-   ##
======================================
  Coverage     100%   100%            
======================================
  Files          87     93     +6     
  Lines        2259   2385   +126     
======================================
+ Hits         2259   2385   +126
```


| [Impacted Files](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tools/deplock2notice/engine\_bow.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2UvZW5naW5lX2Jvdy5nbw==) | `100% <100%> (ø)` | |
| [tools/deplock2notice/noticewriter.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2Uvbm90aWNld3JpdGVyLmdv) | `100% <100%> (ø)` | |
| [tools/deplock2notice/licenseguesser.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2UvbGljZW5zZWd1ZXNzZXIuZ28=) | `100% <100%> (ø)` | |
| [tools/notice2copyright/main.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvbm90aWNlMmNvcHlyaWdodC9tYWluLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [tools/deplock2notice/main.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2UvbWFpbi5nbw==) | `100% <100%> (ø)` | |
| [tools/deplock2notice/assert.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2UvYXNzZXJ0Lmdv) | `100% <100%> (ø)` | |
| [tools/deplock2notice/lockparser.go](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree#diff-dG9vbHMvZGVwbG9jazJub3RpY2UvbG9ja3BhcnNlci5nbw==) | `100% <100%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/bpicode/fritzctl/pull/153/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=footer). Last update [023b457...b42a511](https://codecov.io/gh/bpicode/fritzctl/pull/153?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index 9e29826..e3fa111 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
 coverage-all.html
 /fritzctl
 /notice2copyright
+/deplock2notice
+/analice
 
 # Folders
 _obj
@@ -26,6 +28,7 @@ _testmain.go
 *.exe
 *.test
 *.prof
+*.tmp
 
 # build folder
 /build
diff --git a/Makefile b/Makefile
index 83fa480..1c61a4a 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,8 @@ clean:
 	@rm -f ./coverage-all.out
 	@rm -f ./coverage.out
 	@rm -rf ./build/
+	@rm -f ./fritzctl
+	@rm -f ./analice
 	@$(call ok)
 
 deps:
@@ -98,10 +100,22 @@ man:
 	@gzip --force $(MAN_PAGE_OUTPUT)
 	@$(call ok)
 
-copyright:
+analice:
+	@echo -n ">> ANALICE"
+	@go build github.com/bpicode/fritzctl/tools/analice
+	@$(call ok)
+
+license_compliance: analice
+	@echo -n ">> OSS LICENSE COMPLIANCE"
+	@./analice generate notice . > NOTICE.tmp
+	@diff NOTICE NOTICE.tmp || exit 1
+	@rm NOTICE.tmp
+	@$(call ok)
+
+copyright: license_compliance
 	@echo -n ">> COPYRIGHT, output = $(COPYRIGHT_OUTPUT)"
-	@go build github.com/bpicode/fritzctl/tools/notice2copyright
-	@./notice2copyright ./ "MIT License (Expat)"> $(COPYRIGHT_OUTPUT)
+	@go build github.com/bpicode/fritzctl/tools/analice
+	@./analice generate copyright ./ > $(COPYRIGHT_OUTPUT)
 	@$(call ok)
 
 codequality:
diff --git a/tools/analice/README.md b/tools/analice/README.md
new file mode 100644
index 0000000..6618f07
--- /dev/null
+++ b/tools/analice/README.md
@@ -0,0 +1,37 @@
+# analice
+
+## About
+
+Get an overview of OSS licenses used in your `go` project. 
+
+## Build
+
+```sh
+go build
+```
+
+## Usage
+
+### Generate `NOTICE` file
+```sh
+analice generate notice /path/to/github.com/user/project
+```
+
+### Generate [debian copyright file](https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/).
+```sh
+analice generate copyright /path/to/github.com/user/project
+```
+
+## Assumptions
+
+* Root project uses `dep` as dependency management, `Gopkg.lock` need to be present in root directory.
+* Every dependency is licensed under one license known to this tool.
+* All dependencies need to be in the vendor folder including their `LICENSE` files.
+
+## Known limitations
+
+* Skips dependencies without license, either because they don't have one or it is absent in vendor folder.
+* Skips dependencies which bury the license file in a non-standard location, e.g. by embedding it in `README`.
+* Sometimes does not work for dependencies with heavy re-formatting of the `LICENSE` file, e.g. by using markdown, prefixing every line wth "> ", etc.
+  Line breaks and spaces are OK though.
+* A quick test showed this tool to work on ~50% of the projects in my `GOPATH`.
diff --git a/tools/analice/analyzer.go b/tools/analice/analyzer.go
new file mode 100644
index 0000000..0e34a53
--- /dev/null
+++ b/tools/analice/analyzer.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+)
+
+type analyzer struct {
+	lineAnalyzers []lineAnalyzer
+}
+
+type lineAnalyzer interface {
+	initialize()
+	start(p project, d dependency) error
+	analyze(line string) error
+	stop(p project, d dependency) error
+}
+
+func (l *analyzer) register(a lineAnalyzer) {
+	l.lineAnalyzers = append(l.lineAnalyzers, a)
+}
+
+func (l *analyzer) run(p project) {
+	l.initialize()
+	l.analyzeRoot(p)
+	l.analyzeDeps(p)
+}
+
+func (l *analyzer) initialize() {
+	for _, a := range l.lineAnalyzers {
+		a.initialize()
+	}
+}
+
+func (l *analyzer) analyzeRoot(p project) {
+	empty := dependency{}
+	l.start(p, empty)
+	l.analyzeDir(p.root, p, empty)
+	l.stop(p, empty)
+}
+
+func (l *analyzer) start(p project, d dependency) {
+	for _, a := range l.lineAnalyzers {
+		a.start(p, d)
+	}
+}
+
+func (l *analyzer) stop(p project, d dependency) {
+	for _, a := range l.lineAnalyzers {
+		err := a.stop(p, d)
+		if err != nil {
+			log.Printf("problem analyzing license for '%s': %v", d.name, err)
+		}
+	}
+}
+
+func (l *analyzer) analyzeDeps(p project) {
+	for _, d := range p.dependencies {
+		l.start(p, d)
+		err := l.analyzeDep(p, d)
+		if err != nil {
+			log.Printf("problem analyzing license for '%s': %v", d.name, err)
+			continue
+		}
+		l.stop(p, d)
+	}
+}
+
+func (l *analyzer) analyzeDep(p project, d dependency) error {
+	dir := path.Join(p.root, "vendor", d.name)
+	return l.analyzeDir(dir, p, d)
+}
+
+func (l *analyzer) analyzeDir(dir string, p project, d dependency) error {
+	file, err := l.openLicense(dir)
+	if err != nil {
+		return fmt.Errorf("unable to open license file in directory '%s': %v", dir, err)
+	}
+	defer file.Close()
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		for _, a := range l.lineAnalyzers {
+			a.analyze(line)
+		}
+	}
+	return nil
+}
+
+func (l *analyzer) openLicense(dir string) (*os.File, error) {
+	for _, name := range licenseFiles {
+		f, err := os.Open(filepath.Join(dir, name))
+		if err == nil {
+			return f, nil
+		}
+	}
+	return nil, fmt.Errorf("no license file found in directory '%s' (have tried %v)", dir, licenseFiles)
+}
diff --git a/tools/notice2copyright/copyright_writer.go b/tools/analice/copyright_writer.go
similarity index 55%
rename from tools/notice2copyright/copyright_writer.go
rename to tools/analice/copyright_writer.go
index 1e5282d..31f15c1 100644
--- a/tools/notice2copyright/copyright_writer.go
+++ b/tools/analice/copyright_writer.go
@@ -9,45 +9,65 @@ import (
 	"strings"
 )
 
+// codebeat:disable[TOO_MANY_IVARS]
 type copyrightWriter struct {
-	root project
-	deps []project
+	project              project
+	rootLic              license
+	rootCopyrightHolders []string
+	deps                 []copyrightedLicenseDep
+}
+
+// codebeat:enable[TOO_MANY_IVARS]
+
+type copyrightedLicenseDep struct {
+	dep dependency
+	cph []string
+	lic license
 }
 
 func (c *copyrightWriter) writeTo(w io.Writer) {
 	c.writeHead(w)
-	c.writeProject(w, c.root)
+	c.writeRootProject(w)
 	c.writeDeps(w)
 	c.writeLics(w)
 }
 
 func (c *copyrightWriter) writeHead(w io.Writer) {
 	fmt.Fprintln(w, "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/")
-	fmt.Fprintf(w, "Upstream-Name: %s\n", c.root.name)
-	fmt.Fprintf(w, "Source: %s\n", c.root.url)
+	fmt.Fprintf(w, "Upstream-Name: %s\n", c.project.short())
+	fmt.Fprintf(w, "Source: %s\n", c.project.url())
 	fmt.Fprintln(w, "")
 }
 
-func (c *copyrightWriter) writeProject(w io.Writer, p project) {
-	if p.isRoot {
-		fmt.Fprintln(w, "Files: *")
-	} else {
-		fmt.Fprintf(w, "Files: %s/*\n", p.dir())
-	}
-
-	fmt.Fprintf(w, "Copyright: %s\n", p.copyrightHolders[0])
-	for _, other := range p.copyrightHolders[1:] {
-		fmt.Fprintf(w, "           %s\n", other)
-	}
-	fmt.Fprintf(w, "License: %s\n\n", p.license.shortName)
+func (c *copyrightWriter) writeRootProject(w io.Writer) {
+	fmt.Fprintln(w, "Files: *")
+	c.writeCph(w, c.rootCopyrightHolders)
+	fmt.Fprintf(w, "License: %s\n\n", c.rootLic.shortName)
 }
 
 func (c *copyrightWriter) writeDeps(w io.Writer) {
 	sort.Slice(c.deps, func(i, j int) bool {
-		return c.deps[i].name < c.deps[j].name
+		return c.deps[i].dep.name < c.deps[j].dep.name
 	})
 	for _, d := range c.deps {
-		c.writeProject(w, d)
+		c.writeDep(w, d)
+	}
+}
+
+func (c *copyrightWriter) writeDep(w io.Writer, d copyrightedLicenseDep) {
+	fmt.Fprintf(w, "Files: vendor/%s/*\n", d.dep.name)
+	c.writeCph(w, d.cph)
+	fmt.Fprintf(w, "License: %s\n\n", d.lic.shortName)
+}
+
+func (c *copyrightWriter) writeCph(w io.Writer, cph []string) {
+	if len(cph) == 0 {
+		fmt.Fprintf(w, "Copyright: %s\n", "unknown")
+		return
+	}
+	fmt.Fprintf(w, "Copyright: %s\n", cph[0])
+	for _, other := range cph[1:] {
+		fmt.Fprintf(w, "           %s\n", other)
 	}
 }
 
@@ -61,9 +81,9 @@ func (c *copyrightWriter) writeLics(w io.Writer) {
 
 func (c *copyrightWriter) licSet() map[license]interface{} {
 	m := make(map[license]interface{})
-	m[c.root.license] = nil
-	for _, p := range c.deps {
-		m[p.license] = nil
+	m[c.rootLic] = nil
+	for _, d := range c.deps {
+		m[d.lic] = nil
 	}
 	return m
 }
diff --git a/tools/analice/engine_bow.go b/tools/analice/engine_bow.go
new file mode 100644
index 0000000..c300575
--- /dev/null
+++ b/tools/analice/engine_bow.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"math"
+	"strings"
+)
+
+// codebeat:disable[TOO_MANY_IVARS]
+type bowLicenseEngine struct {
+	knownBags  map[license]bow
+	curDep     dependency
+	curBow     bow
+	licsVsDeps map[license][]dependency
+	rootLic    license
+}
+
+// codebeat:enable[TOO_MANY_IVARS]
+
+func (b *bowLicenseEngine) start(p project, d dependency) error {
+	b.curDep = d
+	b.curBow = bow(make(map[string]int))
+	return nil
+}
+
+func (b *bowLicenseEngine) stop(p project, d dependency) error {
+	best, sim := b.bestMatch(b.curBow)
+	if d.name != "" {
+		b.licsVsDeps[best] = append(b.licsVsDeps[best], d)
+	} else {
+		b.rootLic = best
+	}
+	if sim < 0.5 {
+		return fmt.Errorf("best match was '%s' but with poor similarity (%v)", best.name, sim)
+	}
+	return nil
+}
+
+func (b *bowLicenseEngine) analyze(line string) error {
+	fields := strings.Fields(line)
+	for _, field := range fields {
+		b.curBow[field]++
+	}
+	return nil
+}
+
+func (b *bowLicenseEngine) initialize() {
+	b.knownBags = make(map[license]bow)
+	b.licsVsDeps = make(map[license][]dependency)
+	for _, license := range licenses {
+		ltm := b.bag(strings.NewReader(license.text))
+		b.knownBags[license] = ltm
+	}
+}
+
+func (b *bowLicenseEngine) bag(r io.Reader) bow {
+	m := bow(make(map[string]int))
+	scanner := bufio.NewScanner(r)
+	for scanner.Scan() {
+		line := scanner.Text()
+		fields := strings.Fields(line)
+		for _, field := range fields {
+			m[field]++
+		}
+	}
+	return m
+}
+
+func (b *bowLicenseEngine) bestMatch(m bow) (license, float64) {
+	var bestLicense license
+	bestScore := 0.0
+	for l, bag := range b.knownBags {
+		cos := m.cos(bag)
+		if math.Abs(1.0-cos) < math.Abs(1.0-bestScore) {
+			bestScore, bestLicense = cos, l
+		}
+	}
+	return bestLicense, bestScore
+}
+
+type bow map[string]int
+
+func (x bow) dot(y bow) int {
+	prod := 0
+	for k, v := range x {
+		prod += v * y[k]
+	}
+	return prod
+}
+
+func (x bow) cos(y bow) float64 {
+	xy := float64(x.dot(y))
+	xx := float64(x.dot(x))
+	yy := float64(y.dot(y))
+	return xy / (math.Sqrt(xx * yy))
+}
diff --git a/tools/analice/engine_copyright.go b/tools/analice/engine_copyright.go
new file mode 100644
index 0000000..3c22cd0
--- /dev/null
+++ b/tools/analice/engine_copyright.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+	"regexp"
+	"strings"
+)
+
+type copyrightEngine struct {
+	rootCopyrightHolders   []string
+	depsVsCopyrightHolders map[dependency][]string
+	curDep                 dependency
+	curCopyrightHolders    []string
+}
+
+func (c *copyrightEngine) initialize() {
+	c.depsVsCopyrightHolders = make(map[dependency][]string)
+}
+
+func (c *copyrightEngine) start(p project, d dependency) error {
+	c.curDep = d
+	c.curCopyrightHolders = make([]string, 0)
+	return nil
+}
+
+func (c *copyrightEngine) stop(p project, d dependency) error {
+	holders := make([]string, len(c.curCopyrightHolders))
+	copy(holders, c.curCopyrightHolders)
+	if d.name == "" {
+		c.rootCopyrightHolders = holders
+	} else {
+		c.depsVsCopyrightHolders[d] = holders
+	}
+	return nil
+}
+
+var copyrightRegex = regexp.MustCompile(`^>*\s*[Cc]opyright\s(\([Cc]\)|©)*(?P<CLINE>.*)`)
+
+var copyrightNoAuthorRegex = regexp.MustCompile(`([Cc]opyright notice|[Cc]opyright license to reproduce)`)
+
+func (c *copyrightEngine) analyze(line string) error {
+	if copyrightRegex.MatchString(line) && !copyrightNoAuthorRegex.MatchString(line) {
+		c.curCopyrightHolders = append(c.curCopyrightHolders, c.parseCopyrightHolder(line))
+	}
+	return nil
+}
+
+func (c *copyrightEngine) parseCopyrightHolder(text string) string {
+	matches := copyrightRegex.FindStringSubmatch(text)
+	return strings.TrimSpace(matches[len(matches)-1])
+}
diff --git a/tools/analice/generate.go b/tools/analice/generate.go
new file mode 100644
index 0000000..08f08cb
--- /dev/null
+++ b/tools/analice/generate.go
@@ -0,0 +1,23 @@
+package main
+
+import "github.com/spf13/cobra"
+
+var generateCmd = &cobra.Command{
+	Use: "generate [subcommand]",
+}
+
+func init() {
+	rootCmd.AddCommand(generateCmd)
+}
+
+func projectDir(args []string) string {
+	var dir = "."
+	if len(args) > 0 {
+		dir = args[0]
+	}
+	return dir
+}
+
+func getProjector() projector {
+	return &depLockProjector{}
+}
diff --git a/tools/analice/generate_copyright.go b/tools/analice/generate_copyright.go
new file mode 100644
index 0000000..117f4f7
--- /dev/null
+++ b/tools/analice/generate_copyright.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var copyrightCmd = &cobra.Command{
+	Use:  "copyright /path/to/project",
+	RunE: genCopyright,
+}
+
+func init() {
+	generateCmd.AddCommand(copyrightCmd)
+}
+
+func genCopyright(_ *cobra.Command, args []string) error {
+	dir := projectDir(args)
+	p, err := getProjector().project(dir)
+	if err != nil {
+		return fmt.Errorf("failed to get project dependencies: %v", err)
+	}
+	genCopyrightOf(p)
+	return nil
+}
+
+func genCopyrightOf(p *project) {
+	bowEngine := bowLicenseEngine{}
+	cprEngine := copyrightEngine{}
+	a := newAnalyzer(&bowEngine, &cprEngine)
+	a.run(*p)
+	c := cprWriter(*p, bowEngine, cprEngine)
+	c.writeTo(os.Stdout)
+}
+
+func cprWriter(p project, b bowLicenseEngine, c copyrightEngine) copyrightWriter {
+	ds := joinLicAndCph(b, c)
+	w := copyrightWriter{
+		project:              p,
+		rootCopyrightHolders: c.rootCopyrightHolders,
+		rootLic:              b.rootLic,
+		deps:                 ds,
+	}
+	return w
+}
+
+func joinLicAndCph(b bowLicenseEngine, c copyrightEngine) []copyrightedLicenseDep {
+	depsVsLic := transposeLicDep(b.licsVsDeps)
+	var ds []copyrightedLicenseDep
+	for d, l := range depsVsLic {
+		cld := copyrightedLicenseDep{}
+		cld.dep = d
+		cld.lic = l
+		cld.cph = c.depsVsCopyrightHolders[d]
+		ds = append(ds, cld)
+	}
+	return ds
+}
+
+func transposeLicDep(m map[license][]dependency) map[dependency]license {
+	t := make(map[dependency]license)
+	for l, ds := range m {
+		for _, d := range ds {
+			t[d] = l
+		}
+	}
+	return t
+}
diff --git a/tools/analice/generate_notice.go b/tools/analice/generate_notice.go
new file mode 100644
index 0000000..9b1fa88
--- /dev/null
+++ b/tools/analice/generate_notice.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+var noticeCmd = &cobra.Command{
+	Use:  "notice /path/to/project",
+	RunE: genNotice,
+}
+
+func init() {
+	generateCmd.AddCommand(noticeCmd)
+}
+
+func genNotice(_ *cobra.Command, args []string) error {
+	dir := projectDir(args)
+	p, err := getProjector().project(dir)
+	if err != nil {
+		return fmt.Errorf("failed to get project dependencies: %v", err)
+	}
+	m := runEngine(p)
+	n := noticeWriter{}
+	n.writeTo(os.Stdout, m)
+	return nil
+}
+
+func runEngine(p *project) map[string][]dependency {
+	bowEngine := bowLicenseEngine{}
+	a := newAnalyzer(&bowEngine)
+	a.run(*p)
+	lvd := bowEngine.licsVsDeps
+	m := make(map[string][]dependency)
+	for k, v := range lvd {
+		m[k.name] = append(m[k.name], v...)
+	}
+	return m
+}
+
+func newAnalyzer(las ...lineAnalyzer) analyzer {
+	a := analyzer{}
+	for _, la := range las {
+		a.register(la)
+	}
+	return a
+}
diff --git a/tools/analice/license.go b/tools/analice/license.go
new file mode 100644
index 0000000..9a4b52d
--- /dev/null
+++ b/tools/analice/license.go
@@ -0,0 +1,337 @@
+package main
+
+var licenseFiles = []string{"LICENSE", "LICENSE.md", "LICENSE.txt", "LICENSE.rst", "COPYING", "License", "MIT-LICENSE.txt"}
+
+type license struct {
+	name      string
+	shortName string
+	text      string
+}
+
+var licenses = []license{
+	{
+		name:      "MIT License (Expat)",
+		shortName: "Expat",
+		text: `Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+`},
+	{
+		name:      "Apache License 2.0 (Apache-2.0)",
+		shortName: "Apache-2.0",
+		text: `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.
+`,
+	},
+	{
+		name:      "Apache License 2.0 (Apache-2.0)",
+		shortName: "Apache-2.0",
+		text: `                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   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.
+`},
+	{
+		name:      "ISC License",
+		shortName: "ISC",
+		text: `Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+`},
+	{
+		name:      "BSD 3-Clause License (Revised)",
+		shortName: "BSD-3-clause",
+		text: `Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the following disclaimer
+  in the documentation and/or other materials provided with the
+  distribution.
+* Neither the name of Google Inc. nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+`},
+	{
+		name:      "BSD 2-Clause License (FreeBSD/Simplified)",
+		shortName: "BSD-2-clause",
+		text: `Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+`},
+	{
+		name:      "Do What The F*ck You Want To Public License v2 (WTFPL-2.0)",
+		shortName: "WTFPL-2.0",
+		text: `            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+                    Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar
+  14 rue de Plaisance, 75014 Paris, France
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. You just DO WHAT THE FUCK YOU WANT TO.
+`,
+	},
+}
diff --git a/tools/analice/main.go b/tools/analice/main.go
new file mode 100644
index 0000000..033f974
--- /dev/null
+++ b/tools/analice/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"log"
+
+	"github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+	Use: "analice [subcommand]",
+}
+
+func init() {
+	cobra.OnInitialize()
+	rootCmd.InitDefaultHelpFlag()
+	rootCmd.InitDefaultHelpCmd()
+}
+
+var exitOnErr = log.Fatalln
+
+func main() {
+	err := rootCmd.Execute()
+	if err != nil {
+		exitOnErr(err)
+	}
+}
diff --git a/tools/analice/noticewriter.go b/tools/analice/noticewriter.go
new file mode 100644
index 0000000..5213d40
--- /dev/null
+++ b/tools/analice/noticewriter.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"sort"
+)
+
+type noticeWriter struct {
+}
+
+type section struct {
+	license      string
+	dependencies []dependency
+}
+
+func (n *noticeWriter) writeTo(w io.Writer, m map[string][]dependency) {
+	n.writeIntroWords(w)
+	secs := n.sort(m)
+	writeLicenseBlocks(w, secs)
+}
+
+func (n *noticeWriter) writeIntroWords(w io.Writer) {
+	fmt.Fprintf(w, "%s\n\n", "# 3rd-Party Software License Notice")
+	fmt.Fprintf(w, "%s\n\n", "This software includes the following software and licenses:")
+}
+
+func (n *noticeWriter) sort(m map[string][]dependency) []section {
+	var secs []section
+	for k, v := range m {
+		sec := n.ordered(k, v)
+		secs = append(secs, sec)
+	}
+	sort.Slice(secs, func(i, j int) bool {
+		return secs[i].license < secs[j].license
+	})
+	return secs
+}
+
+func (n *noticeWriter) ordered(lic string, deps []dependency) section {
+	sec := section{license: lic, dependencies: make([]dependency, len(deps))}
+	copy(sec.dependencies, deps)
+	sort.Slice(sec.dependencies, func(i, j int) bool {
+		return sec.dependencies[i].short() < sec.dependencies[j].short()
+	})
+	return sec
+}
+
+func writeLicenseBlocks(w io.Writer, secs []section) {
+	for _, s := range secs {
+		fmt.Fprintf(w, "%s\n", "========================================================================")
+		fmt.Fprintf(w, "%s\n", s.license)
+		fmt.Fprintf(w, "%s\n\n", "========================================================================")
+		fmt.Fprintf(w, "%s\n\n", "The following software have components provided under the terms of this license:")
+
+		for _, d := range s.dependencies {
+			fmt.Fprintf(w, "- %s (from https://%s)\n", d.short(), d.name)
+		}
+		fmt.Fprintln(w)
+	}
+}
diff --git a/tools/analice/project.go b/tools/analice/project.go
new file mode 100644
index 0000000..e11bf3f
--- /dev/null
+++ b/tools/analice/project.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+type project struct {
+	root         string
+	dependencies []dependency
+}
+
+func (p *project) short() string {
+	name, _, _ := p.tokens()
+	return name
+}
+
+func (p *project) url() string {
+	name, user, host := p.tokens()
+	return fmt.Sprintf("https://%s/%s/%s", host, user, name)
+}
+
+func (p *project) tokens() (string, string, string) {
+	abs, _ := filepath.Abs(p.root)
+	split := strings.Split(abs, string(filepath.Separator))
+	padded := append([]string{"", "", ""}, split...)
+	return padded[len(padded)-1], padded[len(padded)-2], padded[len(padded)-3]
+}
+
+type dependency struct {
+	name string
+}
+
+func (d *dependency) short() string {
+	sp := strings.Split(d.name, "/")
+	return sp[len(sp)-1]
+}
+
+type projector interface {
+	project(dir string) (*project, error)
+}
+
+type depLockProjector struct {
+}
+
+func (dlp *depLockProjector) project(dir string) (*project, error) {
+	join := path.Join(dir, "Gopkg.lock")
+	file, err := os.Open(join)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open Gopkg.lock: %v", err)
+	}
+	defer file.Close()
+	return &project{root: dir, dependencies: dlp.parse(file)}, nil
+}
+
+var projectNameRegex = regexp.MustCompile(`^\s*name\s*=\s*"?(?P<NAME>[^"\s]+|\S+)"?\s*`)
+
+func (dlp *depLockProjector) parse(r io.Reader) []dependency {
+	s := bufio.NewScanner(r)
+	var deps []dependency
+	for s.Scan() {
+		text := s.Text()
+		deps = dlp.condAppendProject(deps, text)
+	}
+	return deps
+}
+
+func (dlp *depLockProjector) condAppendProject(deps []dependency, text string) []dependency {
+	if projectNameRegex.MatchString(text) {
+		n := dlp.parseName(text)
+		deps = append(deps, dependency{name: n})
+	}
+	return deps
+}
+
+func (dlp *depLockProjector) parseName(text string) string {
+	matches := projectNameRegex.FindStringSubmatch(text)
+	return strings.TrimSpace(matches[len(matches)-1])
+}
diff --git a/tools/notice2copyright/README.md b/tools/notice2copyright/README.md
deleted file mode 100644
index 3591971..0000000
--- a/tools/notice2copyright/README.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# notice2copyright
-
-## About
-
-Parse a `NOTICE` file to generate the [debian copyright file](https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/).
-
-## Build
-
-```sh
-go build
-```
-
-## Usage
-
-```sh
-notice2copyright path/to/github.com/user/project "MIT License (Expat)"
-```
-
-## Assumptions
-
-* Every dependency is licensed under one license known to this tool.
-* The project has a `NOTICE` file in the following format:
-  ```text
-  ...
-  
-  ========================================================================
-  Apache License 2.0 (Apache-2.0)
-  ========================================================================
-  
-  The following software have components provided under the terms of this license:
-  
-  - cobra (from https://github.com/spf13/cobra)
-
-  ========================================================================
-  MIT License (Expat)
-  ========================================================================
-  
-  The following software have components provided under the terms of this license:
-  
-  - color (from https://github.com/fatih/color)
-
-  ...
-  ```
-  The file needs to reside at the top-level directory of the project.
-* Vendoring. All dependencies need to be in the vendor folder including their `LICENSE` files.
-  A dependency referenced via `- cobra (from https://github.com/spf13/cobra)` in the `NOTICE`
-  file has to be present in the vendor folder as `vendor/github.com/spf13/cobra)`.
diff --git a/tools/notice2copyright/assert.go b/tools/notice2copyright/assert.go
deleted file mode 100644
index 440895b..0000000
--- a/tools/notice2copyright/assert.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log"
-)
-
-func assertOrFatal(val bool, f string, v ...interface{}) {
-	assertTrue(log.Fatalln, val, fmt.Sprintf(f, v...))
-}
-
-func assertTrue(fat func(v ...interface{}), val bool, f string) {
-	if !val {
-		fat(f)
-	}
-}
diff --git a/tools/notice2copyright/copyright_holder.go b/tools/notice2copyright/copyright_holder.go
deleted file mode 100644
index eba116b..0000000
--- a/tools/notice2copyright/copyright_holder.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package main
-
-import (
-	"bufio"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-)
-
-var copyrightRegex = regexp.MustCompile(`^>*\s*[Cc]opyright\s(\([Cc]\)|©)*(?P<CLINE>.*)`)
-
-var copyrightNoAuthorRegex = regexp.MustCompile(`([Cc]opyright notice|[Cc]opyright license to reproduce)`)
-
-func findCopyrightHolders(projects []project, dir string) []project {
-	withCpHolders := make([]project, len(projects))
-	for i, p := range projects {
-		withCpHolders[i] = findCopyrightHoldersOf(p, dir)
-	}
-	return withCpHolders
-}
-
-func findCopyrightHoldersOf(p project, dir string) project {
-	srcDir := filepath.Join(dir, p.dir())
-	lf, err := openFirst(srcDir, "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENSE.rst")
-	assertOrFatal(err == nil, "unable to open license file for project '%s' (%s): %v", p.name, p.url, err)
-	defer lf.Close()
-	p.copyrightHolders = readCopyrightHolders(lf)
-	return p
-}
-
-func openFirst(dir string, names ...string) (*os.File, error) {
-	for _, name := range names {
-		f, err := os.Open(filepath.Join(dir, name))
-		if err == nil {
-			return f, nil
-		}
-	}
-	return nil, fmt.Errorf("no license file [%s] found in directory '%s'", names, dir)
-}
-
-func readCopyrightHolders(r io.Reader) []string {
-	var holders []string
-	s := bufio.NewScanner(r)
-	for s.Scan() {
-		line := s.Text()
-		holders = addCopyRightHolder(line, holders)
-	}
-	if len(holders) == 0 {
-		holders = append(holders, "unknown")
-	}
-	return holders
-}
-
-func addCopyRightHolder(text string, holders []string) []string {
-	if copyrightRegex.MatchString(text) && !copyrightNoAuthorRegex.MatchString(text) {
-		holders = append(holders, parseCopyrightHolder(text))
-	}
-	return holders
-}
-
-func parseCopyrightHolder(text string) string {
-	matches := copyrightRegex.FindStringSubmatch(text)
-	return strings.TrimSpace(matches[len(matches)-1])
-}
diff --git a/tools/notice2copyright/license.go b/tools/notice2copyright/license.go
deleted file mode 100644
index e16cebb..0000000
--- a/tools/notice2copyright/license.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package main
-
-type license struct {
-	name      string
-	shortName string
-	text      string
-}
-
-var (
-	mit = license{
-		shortName: "Expat",
-		name:      "MIT License (Expat)",
-		text: `Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-`}
-	apache2 = license{
-		shortName: "Apache-2.0",
-		name:      "Apache License 2.0 (Apache-2.0)",
-		text: `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.
-
-On Debian systems, the complete text of the Apache 2.0 License
-can be found in /usr/share/common-licenses/Apache-2.0 file.
-`}
-
-	isc = license{
-		shortName: "ISC",
-		name:      "ISC License",
-		text: `Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-`}
-
-	bsd3 = license{
-		shortName: "BSD-3-clause",
-		name:      "BSD 3-Clause License (Revised)",
-		text: `Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright
-  notice, this list of conditions and the following disclaimer.
-* Redistributions in binary form must reproduce the above
-  copyright notice, this list of conditions and the following disclaimer
-  in the documentation and/or other materials provided with the
-  distribution.
-* Neither the name of Google Inc. nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-`}
-
-	bsd2 = license{
-		shortName: "BSD-2-clause",
-		name:      "BSD 2-Clause License (FreeBSD/Simplified)",
-		text: `Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-`}
-)
-
-var licenses = map[string]license{
-	"Apache License 2.0 (Apache-2.0)":           apache2,
-	"MIT License (Expat)":                       mit,
-	"ISC License":                               isc,
-	"BSD 3-Clause License (Revised)":            bsd3,
-	"BSD 2-Clause License (FreeBSD/Simplified)": bsd2,
-}
diff --git a/tools/notice2copyright/main.go b/tools/notice2copyright/main.go
deleted file mode 100644
index 31803c9..0000000
--- a/tools/notice2copyright/main.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-func main() {
-	assertOrFatal(len(os.Args) >= 3, "usage notice2copyright <root project directory> <root project license>")
-	root := parseRoot(os.Args[1])
-	deps := parseNotice(filepath.Join(os.Args[1], "NOTICE"))
-	deps = findCopyrightHolders(deps, filepath.Dir(os.Args[1]))
-	writer := copyrightWriter{root: root, deps: deps}
-	writer.writeTo(os.Stdout)
-}
-
-func parseRoot(dir string) project {
-	abs, err := filepath.Abs(dir)
-	assertOrFatal(err == nil, "cannot determine rot project dir: %v", err)
-	dirs := strings.Split(abs, string(filepath.Separator))
-	root := parseRootFromDirHierarchy(dirs)
-	return findCopyrightHoldersOf(root, dir)
-}
-
-func parseRootFromDirHierarchy(dirs []string) project {
-	assertOrFatal(len(dirs) >= 3, "cannot determine root project: project should reside in a folder like '.../github.com/user/project', got instead %s", dirs)
-	name, user, host := dirs[len(dirs)-1], dirs[len(dirs)-2], dirs[len(dirs)-3]
-	url := fmt.Sprintf("https://%s/%s/%s", host, user, name)
-	p := project{
-		url:     url,
-		name:    name,
-		license: licenses[os.Args[2]],
-		isRoot:  true,
-	}
-	return p
-}
diff --git a/tools/notice2copyright/notice.go b/tools/notice2copyright/notice.go
deleted file mode 100644
index 20c3b16..0000000
--- a/tools/notice2copyright/notice.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package main
-
-import (
-	"bufio"
-	"bytes"
-	"io"
-	"io/ioutil"
-	"os"
-	"regexp"
-)
-
-var projectRegex = regexp.MustCompile(`\s*-*\s*(?P<NAME>\S+)\s*\(\s*from\s*(?P<URL>\S+)\).*`)
-
-func parseNotice(name string) []project {
-	s := readNotice(name)
-	projects := collectProjects(bytes.NewBuffer(s))
-	return projects
-}
-
-func readNotice(name string) []byte {
-	f, err := os.Open(name)
-	assertOrFatal(err == nil, "failed to open '%s': %v", name, err)
-	defer f.Close()
-	s, err := ioutil.ReadAll(f)
-	assertOrFatal(err == nil, "unable to read '%s: %v", name, err)
-	return s
-}
-
-func collectProjects(r io.Reader) []project {
-	s := bufio.NewScanner(r)
-	var projects []project
-	var curLic license
-	for s.Scan() {
-		text := s.Text()
-		updateCurrentLicense(text, &curLic)
-		projects = addProject(projects, text, curLic)
-	}
-	return projects
-}
-
-func addProject(projects []project, text string, lic license) []project {
-	if tokens := projectRegex.FindStringSubmatch(text); len(tokens) == 3 {
-		projects = append(projects, project{license: lic, name: tokens[1], url: tokens[2]})
-	}
-	return projects
-}
-
-func updateCurrentLicense(text string, current *license) {
-	if lic, ok := licenses[text]; ok {
-		*current = lic
-	}
-}
diff --git a/tools/notice2copyright/project.go b/tools/notice2copyright/project.go
deleted file mode 100644
index a19078d..0000000
--- a/tools/notice2copyright/project.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package main
-
-import (
-	"net/url"
-	"path/filepath"
-)
-
-// codebeat:disable[TOO_MANY_IVARS]
-
-type project struct {
-	name             string
-	url              string
-	license          license
-	copyrightHolders []string
-	isRoot           bool
-}
-
-// codebeat:enable[TOO_MANY_IVARS]
-
-func (p *project) dir() string {
-	if p.isRoot {
-		return "."
-	}
-	u, err := url.Parse(p.url)
-	assertOrFatal(err == nil, "unable to parse URL for project '%s' (%s): %v", p.name, p.url, err)
-	return filepath.Join("vendor", u.Host, filepath.FromSlash(u.Path))
-}


Test patch:
diff --git a/tools/analice/analyzer_test.go b/tools/analice/analyzer_test.go
new file mode 100644
index 0000000..3a2a9ad
--- /dev/null
+++ b/tools/analice/analyzer_test.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_licenseGuesser_openLicense(t *testing.T) {
+	a := analyzer{}
+	_, err := a.openLicense("/that/should/not/work/ever")
+	assert.Error(t, err)
+}
+
+func Test_licenseGuesser_guess_file_not_found(t *testing.T) {
+	a := newAnalyzer(&bowLicenseEngine{})
+	p := project{
+		root:         "/points/to/nowhere",
+		dependencies: []dependency{{name: "github.com/acme/something"}},
+	}
+	assert.NotPanics(t, func() { a.run(p) })
+}
+
+func Test_licenseGuesser_guess_one_undetermined(t *testing.T) {
+	pRoot, err := ioutil.TempDir("", "analice-test")
+	assert.NoError(t, err)
+	defer os.RemoveAll(pRoot)
+	err = os.MkdirAll(path.Join(pRoot, "vendor", "dependency"), 0755)
+	assert.NoError(t, err)
+	err = ioutil.WriteFile(path.Join(pRoot, "vendor", "dependency", "LICENSE"), nil, 0644)
+	assert.NoError(t, err)
+
+	a := newAnalyzer(&bowLicenseEngine{})
+	assert.NoError(t, err)
+	p := project{
+		root:         pRoot,
+		dependencies: []dependency{{name: "dependency"}},
+	}
+	assert.NotPanics(t, func() { a.run(p) })
+}
diff --git a/tools/analice/engine_copyright_test.go b/tools/analice/engine_copyright_test.go
new file mode 100644
index 0000000..3cb964b
--- /dev/null
+++ b/tools/analice/engine_copyright_test.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_parseCopyrightHolder(t *testing.T) {
+	e := copyrightEngine{}
+	assert.Equal(t, "2016 bpicode", e.parseCopyrightHolder("Copyright (c) 2016 bpicode"))
+	assert.Equal(t, "John Doe <john.d@example.com>", e.parseCopyrightHolder("Copyright (c) John Doe <john.d@example.com>"))
+	assert.Equal(t, "2009 The Go Authors. All rights reserved.", e.parseCopyrightHolder("Copyright (c) 2009 The Go Authors. All rights reserved."))
+	assert.Equal(t, "2011 John Doe <john.d@example.com>", e.parseCopyrightHolder("Copyright © 2011 John Doe <john.d@example.com>"))
+}
diff --git a/tools/analice/generate_notice_test.go b/tools/analice/generate_notice_test.go
new file mode 100644
index 0000000..355bafd
--- /dev/null
+++ b/tools/analice/generate_notice_test.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_noticeCmd_no_project(t *testing.T) {
+	err := genNotice(noticeCmd, []string{"/points/to/nowhere"})
+	assert.Error(t, err)
+}
diff --git a/tools/analice/main_test.go b/tools/analice/main_test.go
new file mode 100644
index 0000000..1dc5671
--- /dev/null
+++ b/tools/analice/main_test.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_generate_notice(t *testing.T) {
+	var argsOrig []string
+	copy(argsOrig, os.Args)
+	defer func() { os.Args = argsOrig }()
+	os.Args = []string{"analice", "generate", "notice", "../.."}
+	assert.NotPanics(t, main)
+}
+
+func Test_generate_copyright(t *testing.T) {
+	var argsOrig []string
+	copy(argsOrig, os.Args)
+	defer func() { os.Args = argsOrig }()
+	os.Args = []string{"analice", "generate", "copyright", "../.."}
+	assert.NotPanics(t, main)
+}
+
+func Test_generate_with_err(t *testing.T) {
+	var argsOrig []string
+	copy(argsOrig, os.Args)
+	defer func() { os.Args = argsOrig }()
+
+	var exOnErr = exitOnErr
+	defer func() { exitOnErr = exOnErr }()
+	exitOnErr = func(v ...interface{}) { panic(v) }
+
+	os.Args = []string{"analice", "generate", "notice", "/good/luck/with/that"}
+	assert.Panics(t, main)
+
+	os.Args = []string{"analice", "generate", "copyright", "/not/gonna/work"}
+	assert.Panics(t, main)
+}
diff --git a/tools/analice/project_test.go b/tools/analice/project_test.go
new file mode 100644
index 0000000..8a47c14
--- /dev/null
+++ b/tools/analice/project_test.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_projectNameRegex(t *testing.T) {
+	assert.True(t, projectNameRegex.MatchString("  name = \"github.com/spf13/pflag\""))
+	assert.True(t, projectNameRegex.MatchString("name=a/b/c"))
+	assert.False(t, projectNameRegex.MatchString("name="))
+	assert.False(t, projectNameRegex.MatchString("name = "))
+	assert.False(t, projectNameRegex.MatchString(`  analyzer-name = "dep"`))
+	assert.False(t, projectNameRegex.MatchString(`  solver-name = "gps-cdcl"`))
+}
+
+func Test_parseName(t *testing.T) {
+	dlp := depLockProjector{}
+	assert.Equal(t, "github.com/spf13/pflag", dlp.parseName("  name = \"github.com/spf13/pflag\""))
+}
diff --git a/tools/notice2copyright/assert_test.go b/tools/notice2copyright/assert_test.go
deleted file mode 100644
index 9d0ba13..0000000
--- a/tools/notice2copyright/assert_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
-	"log"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_assertTrue(t *testing.T) {
-	assert.NotPanics(t, func() {
-		assertTrue(log.Fatalln, true, "err")
-	})
-	assert.Panics(t, func() {
-		assertTrue(func(v ...interface{}) {
-			panic("panic")
-		}, false, "err")
-	})
-}
diff --git a/tools/notice2copyright/copyright_holder_test.go b/tools/notice2copyright/copyright_holder_test.go
deleted file mode 100644
index fd3348d..0000000
--- a/tools/notice2copyright/copyright_holder_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_openFirst(t *testing.T) {
-	_, err := openFirst("/that/should/not/work/ever", "asdfagag.txt", "asasgdsg.md")
-	assert.Error(t, err)
-}
-
-func Test_parseCopyrightHolder(t *testing.T) {
-	assert.Equal(t, "2016 bpicode", parseCopyrightHolder("Copyright (c) 2016 bpicode"))
-	assert.Equal(t, "John Doe <john.d@example.com>", parseCopyrightHolder("Copyright (c) John Doe <john.d@example.com>"))
-	assert.Equal(t, "2009 The Go Authors. All rights reserved.", parseCopyrightHolder("Copyright (c) 2009 The Go Authors. All rights reserved."))
-	assert.Equal(t, "2011 John Doe <john.d@example.com>", parseCopyrightHolder("Copyright © 2011 John Doe <john.d@example.com>"))
-}
diff --git a/tools/notice2copyright/main_test.go b/tools/notice2copyright/main_test.go
deleted file mode 100644
index 440c683..0000000
--- a/tools/notice2copyright/main_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
-	"os"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_main(t *testing.T) {
-	argsOrig := os.Args[:]
-	defer func() { os.Args = argsOrig }()
-	os.Args = []string{"notice2copyright", "../../", "MIT License (Expat)"}
-	assert.NotPanics(t, main)
-}
diff --git a/tools/notice2copyright/notice_test.go b/tools/notice2copyright/notice_test.go
deleted file mode 100644
index 3b0333e..0000000
--- a/tools/notice2copyright/notice_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package main
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_projectRegex(t *testing.T) {
-	s := "- cobra (from https://github.com/spf13/cobra)"
-	matches := projectRegex.FindStringSubmatch(s)
-	assert.Len(t, matches, 3)
-}