From ac5f44589302d797b5aad3eec3c675a400cf731b Mon Sep 17 00:00:00 2001 From: Marc Lopez Rubio Date: Fri, 9 Aug 2019 16:21:21 +0800 Subject: [PATCH] Add -notice flag to autogenerate NOTICE files (#25) Adds a -notice flag and a subset of flags tied to -notice. When the flag is set, generates a `NOTICE` file at the root folder of the project when used with or stdout when using `-d` for dry runs. The NOTICE file generation heavily relies on the `go.mod` file and its contents, Any other dependency managers outside of those reliant on `go.mod` won't be able to make use of this feature. By enabling this feature, the binary won't be zery dependency anymore even though it's still very fast. The format is fixed unless the Go template is overriden with the flag `-notice-header`, in which case only the header is overwritten, maintaining the list of dependencies and licenses within enclosed `=`. It also changes the default error code from `0` to `255` any time the error is not nil. Before any unspecified errorcodes defaulted to 0 which is not the desired behaviour. Signed-off-by: Marc Lopez --- .travis.yml | 12 +- Makefile | 15 +- NOTICE | 17 +- README.md | 20 ++- appveyor.yml | 9 +- error.go | 5 +- error_test.go | 4 +- fixtures/go.mod | 9 + go.mod | 9 + go.sum | 118 +++++++++++++ golden/NOTICE | 13 ++ golden/go.mod | 9 + licensing/notice.go | 293 ++++++++++++++++++++++++++++++++ licensing/notice_test.go | 189 ++++++++++++++++++++ licensing/testfiles/nodeps.mod | 3 + licensing/testfiles/twodeps.mod | 8 + main.go | 88 +++++++++- main_test.go | 118 ++++++++++--- notice.go | 87 ++++++++++ 19 files changed, 971 insertions(+), 55 deletions(-) create mode 100644 fixtures/go.mod create mode 100644 go.sum create mode 100644 golden/NOTICE create mode 100644 golden/go.mod create mode 100644 licensing/notice.go create mode 100644 licensing/notice_test.go create mode 100644 licensing/testfiles/nodeps.mod create mode 100644 licensing/testfiles/twodeps.mod create mode 100644 notice.go diff --git a/.travis.yml b/.travis.yml index ba9a4e5..9ed106f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,16 @@ language: go go: - - "1.x" - - "1.9.x" - - "1.10.x" - "1.11.x" + - "1.12.x" + +env: + - GO111MODULE=on + +cache: + directories: + - $HOME/.cache/go-build + - $HOME/gopath/pkg/mod matrix: fast_finish: true diff --git a/Makefile b/Makefile index c60e387..1040d73 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ export VERSION := 0.2.0 +export GO111MODULE ?= on OWNER ?= elastic REPO ?= go-licenser TEST_UNIT_FLAGS ?= -timeout 10s -p 4 -race -cover @@ -8,6 +9,7 @@ GOIMPORTS_PRESENT := $(shell command -v goimports 2> /dev/null) GORELEASER_PRESENT := $(shell command -v goreleaser 2> /dev/null) RELEASED = $(shell git tag -l $(VERSION)) DEFAULT_LDFLAGS ?= -X main.version=$(VERSION)-dev -X main.commit=$(shell git rev-parse HEAD) +LICENSER_FORMAT_FLAGS ?= -notice -notice-year 2018 -notice-project-name "Go Licenser" define HELP ///////////////////////////////////////// @@ -21,7 +23,7 @@ define HELP ## Development targets -- deps: It will install the dependencies required to run developemtn targets. +- deps: It will install the dependencies required to run development targets. - unit: Runs the unit tests. - lint: Runs the linters. - format: Formats the source files according to gofmt, goimports and go-licenser. @@ -43,11 +45,12 @@ help: .PHONY: deps deps: ifndef GOLINT_PRESENT - @ go get -u golang.org/x/lint/golint + @ GO111MODULE=off go get -u golang.org/x/lint/golint endif ifndef GOIMPORTS_PRESENT - @ go get -u golang.org/x/tools/cmd/goimports + @ GO111MODULE=off go get -u golang.org/x/tools/cmd/goimports endif + @ go mod download .PHONY: release_deps release_deps: @@ -68,11 +71,11 @@ unit: .PHONY: build build: deps - @ go build -o bin/$(REPO) -ldflags="$(DEFAULT_LDFLAGS)" + @ CGO_ENABLED=0 go build -o bin/$(REPO) -ldflags="$(DEFAULT_LDFLAGS)" .PHONY: install install: deps - @ go install + @ CGO_ENABLED=0 go install .PHONY: lint lint: build @@ -84,7 +87,7 @@ lint: build format: deps build @ gofmt -e -w -s . @ goimports -w . - @ ./bin/go-licenser -exclude golden + @ ./bin/go-licenser -exclude golden $(LICENSER_FORMAT_FLAGS) .PHONY: release release: deps release_deps diff --git a/NOTICE b/NOTICE index 4972cec..b64893d 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,14 @@ -Elastic go-licenser -Copyright 2018 Elasticsearch B.V. +Go Licenser +Copyright 2018-2019 Elasticsearch B.V. -This product includes software developed at -Elasticsearch, B.V. (https://www.elastic.co/). +This product includes software developed at Elasticsearch B.V. and +third-party software developed by the licenses listed below. + +========================================================================= + +gopkg.in/src-d/go-license-detector.v2 Apache-2.0 +golang.org/x/exp BSD-3-Clause +github.com/sirkon/goproxy MIT +github.com/hashicorp/go-multierror MPL-2.0 + +========================================================================= diff --git a/README.md b/README.md index ea3e81c..0c6d490 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Go Licenser [![Build Status](https://travis-ci.org/elastic/go-licenser.svg?branch=master)](https://travis-ci.org/elastic/go-licenser) -Small zero dependency license header checker for source files. The aim of this project is to provide a common -binary that can be used to ensure that code source files contain a license header. It's unlikely that this project -is useful outside of Elastic **_at the current stage_**, but the `licensing` package can be used as a building block. +Small license header checker for source files, mainly thought and tested for Go source files although it might work for other ones. The aim of this project is to provide a common +binary that can be used to ensure that code source files contain a license header. + +Additionally, when the `-notice` flag is set, generates a `NOTICE` file at the root folder of the project when used with or stdout when using `-d` for dry runs. ## Supported Licenses @@ -28,6 +29,9 @@ Usage: go-licenser [flags] [path] go-licenser walks the specified path recursiely and appends a license Header if the current header doesn't match the one found in the file. + Using the -notice flag a compiled list of the project's dependencies and licenses is compiled + after the "go.mod" file is inspected. If the dependencies aren't found locally, it will fail. + Options: -d skips rewriting files and returns exitcode 1 if any discrepancies are found. @@ -39,6 +43,16 @@ Options: sets the license type to check: ASL2, Elastic, Cloud (default "ASL2") -licensor string sets the name of the licensor (default "Elasticsearch B.V.") + -notice + generates a NOTICE (use -notice-file to change it) file on the folder where it's being run. + -notice-file string + specifies the file where to write the license notice. (default "NOTICE") + -notice-header string + specifies the notice header Go Template. + -notice-project-name string + specifies the notice project name at the top of the Go Template (defaults to folder name). + -notice-year string + specifies the start of the project so the notice file reflects it. -version prints out the binary version. ``` diff --git a/appveyor.yml b/appveyor.yml index 2292bcb..4aa6753 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,9 @@ clone_folder: c:\gopath\src\github.com\elastic\go-licenser environment: GOPATH: c:\gopath - GOVERSION: 1.10 + GOVERSION: 1.12 + GO111MODULE: on + CGO_ENABLED: 0 # Build @@ -23,8 +25,11 @@ install: - go env before_build: + - go mod download + - set GO111MODULE=off - go get -u golang.org/x/lint/golint - go get -u golang.org/x/tools/cmd/goimports + - set GO111MODULE=on - golint -set_exit_status . - gofmt -d -e -s . - goimports -d . @@ -35,4 +40,4 @@ build_script: test_script: - go-licenser -d -exclude golden - - go test -timeout 10s -p 4 -race -cover ./... + - go test -timeout 10s -p 4 -cover ./... diff --git a/error.go b/error.go index 5c40faa..02d528e 100644 --- a/error.go +++ b/error.go @@ -32,8 +32,11 @@ func (e Error) Error() string { // Code returns the exitcode for the error func Code(e error) int { + if e == nil { + return 0 + } if err, ok := e.(*Error); ok { return err.code } - return 0 + return 255 } diff --git a/error_test.go b/error_test.go index 38e6cf0..28c9adc 100644 --- a/error_test.go +++ b/error_test.go @@ -37,11 +37,11 @@ func TestCode(t *testing.T) { want: 0, }, { - name: "standard error returns 0", + name: "standard error returns 255", args: args{ e: errors.New("an error"), }, - want: 0, + want: 255, }, { name: "Error error returns 1", diff --git a/fixtures/go.mod b/fixtures/go.mod new file mode 100644 index 0000000..2e87e9f --- /dev/null +++ b/fixtures/go.mod @@ -0,0 +1,9 @@ +module github.com/elastic/go-licenser/fixtures + +go 1.12 + +require ( + github.com/hashicorp/go-multierror v1.0.0 + github.com/sirkon/goproxy v1.4.0 + gopkg.in/src-d/go-license-detector.v2 v2.0.1 +) diff --git a/go.mod b/go.mod index 89b964c..11b82be 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,10 @@ module github.com/elastic/go-licenser + +go 1.12 + +require ( + github.com/hashicorp/go-multierror v1.0.0 + github.com/sirkon/goproxy v1.4.0 + golang.org/x/exp v0.0.0-20190121172915-509febef88a4 // indirect + gopkg.in/src-d/go-license-detector.v2 v2.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb5ab8a --- /dev/null +++ b/go.sum @@ -0,0 +1,118 @@ +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544 h1:54Y/2GF52MSJ4n63HWvNDFRtztgm6tq2UrOX61sjGKc= +github.com/dgryski/go-minhash v0.0.0-20170608043002-7fe510aff544/go.mod h1:VBi0XHpFy0xiMySf6YpVbRqrupW4RprJ5QTyN+XvGSM= +github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2 h1:lx1ZQgST/imDhmLpYDma1O3Cx9L+4Ie4E8S2RjFPQ30= +github.com/dgryski/go-spooky v0.0.0-20170606183049-ed3d087f40e2/go.mod h1:hgHYKsoIw7S/hlWtP7wD1wZ7SX1jPTtKko5X9jrOgPQ= +github.com/ekzhu/minhash-lsh v0.0.0-20171225071031-5c06ee8586a1 h1:/7G7q8SDJdrah5jDYqZI8pGFjSqiCzfSEO+NgqKCYX0= +github.com/ekzhu/minhash-lsh v0.0.0-20171225071031-5c06ee8586a1/go.mod h1:yEtCVi+QamvzjEH4U/m6ZGkALIkF2xfQnFp0BcKmIOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hhatto/gorst v0.0.0-20171128071645-7682c8a25108 h1:wWkhJ3fgjH1kk5Sp9mYd1puH4PVEU/k6GcpVp2LIUZw= +github.com/hhatto/gorst v0.0.0-20171128071645-7682c8a25108/go.mod h1:HmaZGXHdSwQh1jnUlBGN2BeEYOHACLVGzYOXCbsLvxY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jdkato/prose v1.1.0 h1:LpvmDGwbKGTgdCH3a8VJL56sr7p/wOFPw/R4lM4PfFg= +github.com/jdkato/prose v1.1.0/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg= +github.com/kevinburke/ssh_config v0.0.0-20180127194858-0ff8514904a8 h1:klRp3/4ivQOL3+RkXTG7Y0inb9Tw6FNsk6xGicemw98= +github.com/kevinburke/ssh_config v0.0.0-20180127194858-0ff8514904a8/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk= +github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb h1:bsjNADsjHq0gjU7KO7zwoX5k3HtFdf6TDzB3ncl5iUs= +github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/neurosnap/sentences v1.0.6 h1:iBVUivNtlwGkYsJblWV8GGVFmXzZzak907Ci8aA0VTE= +github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc= +github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v0.0.0-20180205163309-da645544ed44 h1:UoZTeCJuGZpwXXqamtSHypwjqpyGdR9smB5iMleBDJ8= +github.com/sergi/go-diff v0.0.0-20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shogo82148/go-shuffle v0.0.0-20170808115208-59829097ff3b h1:VI1u+o2KZPZ5AhuPpXY0JBdpQPnkTx6Dd5XJhK/9MYE= +github.com/shogo82148/go-shuffle v0.0.0-20170808115208-59829097ff3b/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirkon/gitlab v0.0.4/go.mod h1:shZhI7CQWIXV84FhVUPietVUS3OcjOm9/YQwrgyVL0Q= +github.com/sirkon/goproxy v1.4.0 h1:I0P9qEGd7dNXbH3UKXi96RUNN2+hLqdifxIziOHQbag= +github.com/sirkon/goproxy v1.4.0/go.mod h1:/Z/fwyv22M5ge53jtwtXiy4Qd/RRR7ZUQv08mm7+GLY= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg= +github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= +github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.0.0-20180206190813-d9133f546934 h1:UbtgwftcdEk1BsxGUZ2PA880WW9HqmwoQn6MKRkNelQ= +golang.org/x/crypto v0.0.0-20180206190813-d9133f546934/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20171209012058-072991165226 h1:sb67HiWk98LHu6TtgnfVTHraZYkXLgFRJlXcNahrXCc= +golang.org/x/exp v0.0.0-20171209012058-072991165226/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/net v0.0.0-20180208041118-f5dfe339be1d h1:lnO2rP1Eit1fCAJKjYJlnArsHluPBxcs2BA2dQrL224= +golang.org/x/net v0.0.0-20180208041118-f5dfe339be1d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54 h1:a5WocgxWTnjG0C4hZblDx+yonFbQMMbv8yJGhHMz/nY= +golang.org/x/text v0.0.0-20180208041248-4e4a3210bb54/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +gonum.org/v1/gonum v0.0.0-20180205154402-996b88e8f894 h1:7JhBunAsbjfnPGrLN5GFVAqcEs//9Bblx/Fosnaw/TY= +gonum.org/v1/gonum v0.0.0-20180205154402-996b88e8f894/go.mod h1:cucAdkem48eM79EG1fdGOGASXorNZIYAO9duTse+1cI= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/neurosnap/sentences.v1 v1.0.6 h1:v7ElyP020iEZQONyLld3fHILHWOPs+ntzuQTNPkul8E= +gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= +gopkg.in/src-d/go-billy-siva.v4 v4.3.0 h1:bin2telR2qmBfHfTb61SboE4M1LjofSN0g6vn4Tmdck= +gopkg.in/src-d/go-billy-siva.v4 v4.3.0/go.mod h1:4wKeCzOCSsdyFeM5+58M6ObU6FM+lZT12p7zm7A+9n0= +gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= +gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 h1:AxUOwLW3at53ysFqs0Lg+H+8KSQXl7AEHBvWj8wEsT8= +gopkg.in/src-d/go-git-fixtures.v3 v3.3.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.1.0 h1:ca/w7I2ziAYUrqcyq0Sm065FS1EUQKlzDCm09/RaF0o= +gopkg.in/src-d/go-git.v4 v4.1.0/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo= +gopkg.in/src-d/go-license-detector.v2 v2.0.1 h1:F2BH4vJ2gn6ae+p/ageiSdd9x0EJosC2zwugI5DxvIo= +gopkg.in/src-d/go-license-detector.v2 v2.0.1/go.mod h1:a1TpgXtfK7CO102qjOEAbDw50sUD7ObShWTvckedfoU= +gopkg.in/src-d/go-siva.v1 v1.5.0 h1:WowvbZTlz0SPoV7WNCGktPSi2yRK78HPyXl7wYqDeHE= +gopkg.in/src-d/go-siva.v1 v1.5.0/go.mod h1:tk1jnIXawd/PTlRNWdr5V5lC0PttNJmu1fv7wt7IZlw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/golden/NOTICE b/golden/NOTICE new file mode 100644 index 0000000..801dc5e --- /dev/null +++ b/golden/NOTICE @@ -0,0 +1,13 @@ +SomeProject +Copyright 2019 Elasticsearch B.V. + +This product includes software developed at Elasticsearch B.V. and +third-party software developed by the licenses listed below. + +========================================================================= + +gopkg.in/src-d/go-license-detector.v2 Apache-2.0 +github.com/sirkon/goproxy MIT +github.com/hashicorp/multierror-go MPL-2.0 + +========================================================================= diff --git a/golden/go.mod b/golden/go.mod new file mode 100644 index 0000000..2e87e9f --- /dev/null +++ b/golden/go.mod @@ -0,0 +1,9 @@ +module github.com/elastic/go-licenser/fixtures + +go 1.12 + +require ( + github.com/hashicorp/go-multierror v1.0.0 + github.com/sirkon/goproxy v1.4.0 + gopkg.in/src-d/go-license-detector.v2 v2.0.1 +) diff --git a/licensing/notice.go b/licensing/notice.go new file mode 100644 index 0000000..a595196 --- /dev/null +++ b/licensing/notice.go @@ -0,0 +1,293 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 licensing + +import ( + "bytes" + "errors" + "fmt" + "go/build" + "io" + "io/ioutil" + "path/filepath" + "regexp" + "sort" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/sirkon/goproxy/gomod" + "gopkg.in/src-d/go-license-detector.v2/licensedb" +) + +// DefaultNoticeHeader is used as the default NOTICE header. +const DefaultNoticeHeader = `{{.Project}} +Copyright {{.ProjectYears}} {{.Licensor}} + +This product includes software developed at {{.Licensor}} and +third-party software developed by the licenses listed below. +` + +// NoticeFormat is the template format for the generated notice file. +const NoticeFormat = `%s +========================================================================= + +{{.DependencyBlob}} +========================================================================= +` + +// Notice contains all of the information +type Notice struct { + // Licensor is the source code's owner / maintainer. + Licensor string + + // Project is the source code's project or repository name. + Project string + + // ProjectYears contains the years that the project has been running + // in the {{start year}}-{{current year}} or {{year}} format depending + // if it's a single or multi-year effort. + ProjectYears string + + // Dependencies contains the dependency list that can be consumed in a + // programatic way. + Dependencies []Dependency + + // DependencyBlob is a stringified version of Dependencies that has been + // processed through the tabwritter package so the columns are aligned. + // Only populated when Writer is not nil. + DependencyBlob string +} + +// Dependency contains the dependency name and the license. +type Dependency struct { + Name, License string +} + +// GenerateNoticeParams is consumed by GenerateNotice. +type GenerateNoticeParams struct { + // GoModFile is the location of the `go.mod` file to open. + GoModFile string + + // Licensor of the codebase. + Licensor string + + // Project name. + Project string + + // Project's start year so it can be reflected in the NOTICE file. + StartYear int + + // Writer where to write the NOTICE templated output. If not specified, + // writing the template will be skipped, and Notice won't have a populated + // DependencyBlob. + Writer io.Writer + + // NoticeHeader overrides the DefaultNoticeHeader used in NOTICE. Only relevant + // when Writer is not nil. + NoticeHeader string + + // AnalyseFunc returns a list of results with their license. + AnalyseFunc func(args ...string) []licensedb.Result +} + +// Validate ensures that the structure is usable by its consumer. +func (params GenerateNoticeParams) Validate() error { + var merr = new(multierror.Error) + if params.GoModFile == "" { + merr = multierror.Append(merr, errors.New("notice: missing file path")) + } + if params.AnalyseFunc == nil { + merr = multierror.Append(merr, errors.New("notice: missing AnalyseFunc")) + } + if params.Project == "" { + merr = multierror.Append(merr, errors.New("notice: missing project name")) + } + + return merr.ErrorOrNil() +} + +func (params GenerateNoticeParams) fillDefaults() GenerateNoticeParams { + if params.NoticeHeader == "" { + params.NoticeHeader = DefaultNoticeHeader + } + return params +} + +func goPkgPath() string { + return filepath.Join(build.Default.GOPATH, "pkg", "mod") +} + +// GenerateNotice inspects the `go.mod` file and inspects the contents of the +// downloaded dependency to determine which license is most predominant in its +// source files. When a Writer is passed in the parameters, it also writes a +// templated output in the writer. +func GenerateNotice(params GenerateNoticeParams) (Notice, error) { + var notice Notice + if err := params.Validate(); err != nil { + return notice, err + } + + params = params.fillDefaults() + notice = buildNotice(params) + + paths, err := getModulePaths(params.GoModFile) + if err != nil { + return notice, err + } + + notice.Dependencies = getLicenses(params.AnalyseFunc, paths...) + + // Skip template execution. + if params.Writer == nil { + return notice, nil + } + + templateFormat := fmt.Sprintf(NoticeFormat, params.NoticeHeader) + if err := writeTemplate(¬ice, templateFormat, params.Writer); err != nil { + return notice, err + } + return notice, nil +} + +// buildNotice constructs a Notice from the parameters. +func buildNotice(params GenerateNoticeParams) (notice Notice) { + var currentYear = time.Now().Year() + notice = Notice{ + Licensor: params.Licensor, + Project: params.Project, + ProjectYears: fmt.Sprint(params.StartYear, "-", currentYear), + } + if params.StartYear == currentYear || params.StartYear == 0 { + notice.ProjectYears = fmt.Sprint(currentYear) + } + return notice +} + +// getModulePaths opens and parses a `go.mod` file obtaining the list of +// dependencies and constructing a local filesystem path where they have +// been downloaded. The returning slice is sorted. +func getModulePaths(file string) ([]string, error) { + contents, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + module, err := gomod.Parse(file, contents) + if err != nil { + return nil, err + } + + if len(module.Require) == 0 { + return nil, errors.New("modfile has no dependencies to generate notice") + } + + var paths = make([]string, 0, len(module.Require)) + // Each dependency is processed and the full filepath is constructed. + // Additionally, dependencies that have Uppercase characters are converted + // to ! since it's how go modules are downloaded in the local + // filesystem. + for dep, version := range module.Require { + re, err := regexp.Compile("([A-Z]+)") + if err != nil { + return nil, err + } + + for _, letter := range re.FindAllString(dep, -1) { + dep = strings.Replace(dep, letter, + "!"+strings.ToLower(letter), -1, + ) + } + + p := filepath.Join( + append([]string{goPkgPath()}, strings.Split(dep, "/")...)..., + ) + p = fmt.Sprint(p, "@", version) + paths = append(paths, p) + } + + sort.Strings(paths) + return paths, nil +} + +// getLicenses returns a []Dependency which is populated by the analyseFunc. +// It relies on the local downloaded dependencies in order for the license +// discovery mechanism to work. The Dependencies slice is sorted by License. +func getLicenses(analyseFunc func(args ...string) []licensedb.Result, paths ...string) []Dependency { + var dependencies = make([]Dependency, 0, len(paths)) + for _, res := range analyseFunc(paths...) { + // Replaces the prefix in front of the Go dependency name and the + // @ that follows, so that a clean name is returned. + arg := strings.Replace(res.Arg, goPkgPath()+"/", "", -1) + if depv := strings.Split(arg, "@"); len(depv) > 1 { + arg = strings.Replace(arg, "@"+depv[1], "", -1) + } + + // When the license type hasn't been detected, the "Error" is treated + // as the License so it can be reflected. + if len(res.Matches) == 0 { + // Removes the break line at the end of the error string and + // the local goPath if it's there. + res.Matches = []licensedb.Match{{ + License: strings.Replace( + strings.Replace(res.ErrStr, "\n", "", 1), + goPkgPath()+"/", "", -1, + ), + }} + } + + // The ! character is removed for any uppercase dependencies and the + // dependency on the NOTICE will have theose characters lowercased. + dependencies = append(dependencies, Dependency{ + Name: strings.Replace(arg, "!", "", -1), + License: res.Matches[0].License, + }) + } + + sort.SliceStable(dependencies, func(i, j int) bool { + return strings.ToLower(dependencies[i].License) < strings.ToLower(dependencies[j].License) + }) + + return dependencies +} + +// writeTemplate writes a notice templated output by using the specified format +// and on to the writer. The DependenciesBlob field on the passed Notice will +// be populated as: +// {{.Dependency}} {{.License}} +// The tab space will be determined by the length of the dependency name. +func writeTemplate(notice *Notice, format string, writer io.Writer) error { + var buf = new(bytes.Buffer) + var w = tabwriter.NewWriter(buf, 4, 2, 4, ' ', 0) + var deps = notice.Dependencies + for i := range deps { + w.Write([]byte(deps[i].Name + "\t" + deps[i].License + "\n")) + } + + w.Flush() + notice.DependencyBlob = buf.String() + + t, err := template.New("").Parse(format) + if err != nil { + return err + } + + return t.Execute(writer, notice) +} diff --git a/licensing/notice_test.go b/licensing/notice_test.go new file mode 100644 index 0000000..40c58f6 --- /dev/null +++ b/licensing/notice_test.go @@ -0,0 +1,189 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 licensing + +import ( + "bytes" + "errors" + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/hashicorp/go-multierror" + "gopkg.in/src-d/go-license-detector.v2/licensedb" +) + +func genAnalyseFunc(r []licensedb.Result) func(args ...string) []licensedb.Result { + return func(args ...string) []licensedb.Result { return r } +} + +func createGoModFile(path string, contents string) func() { + ioutil.WriteFile(path, []byte(contents), 0777) + + return func() { os.RemoveAll(path) } +} + +const fooCompanyOut = `somedeps +Copyright 2012-2019 FooCompany L.T.D. + +This product includes software developed at FooCompany L.T.D. and +third-party software developed by the licenses listed below. + +========================================================================= + +gopkg.in/src-d/go-license-detector.v2 Apache-2.0 +github.com/hashicorp/multierror-go MPL-2.0 + +========================================================================= +` + +func TestGenerateNotice(t *testing.T) { + type args struct { + params GenerateNoticeParams + withBuffer bool + } + tests := []struct { + name string + args args + want Notice + wantOut string + err error + }{ + { + name: "parses go.mod with two dependencies and returns the findings", + args: args{params: GenerateNoticeParams{ + GoModFile: "testfiles/twodeps.mod", + AnalyseFunc: genAnalyseFunc([]licensedb.Result{ + { + Arg: "github.com/hashicorp/multierror-go", + Matches: []licensedb.Match{{License: "MPL-2.0"}}, + }, + { + Arg: "gopkg.in/src-d/go-license-detector.v2", + Matches: []licensedb.Match{{License: "Apache-2.0"}}, + }, + }), + Project: "somedeps", + }}, + want: Notice{Project: "somedeps", ProjectYears: "2019", Dependencies: []Dependency{ + {Name: "gopkg.in/src-d/go-license-detector.v2", License: "Apache-2.0"}, + {Name: "github.com/hashicorp/multierror-go", License: "MPL-2.0"}, + }}, + }, + { + name: "parses go.mod with two dependencies with a writer", + args: args{withBuffer: true, params: GenerateNoticeParams{ + Licensor: "FooCompany L.T.D.", + StartYear: 2012, + GoModFile: "testfiles/twodeps.mod", + AnalyseFunc: genAnalyseFunc([]licensedb.Result{ + { + Arg: "github.com/hashicorp/multierror-go", + Matches: []licensedb.Match{{License: "MPL-2.0"}}, + }, + { + Arg: "gopkg.in/src-d/go-license-detector.v2", + Matches: []licensedb.Match{{License: "Apache-2.0"}}, + }, + }), + Project: "somedeps", + }}, + want: Notice{Project: "somedeps", ProjectYears: "2012-2019", Licensor: "FooCompany L.T.D.", + Dependencies: []Dependency{ + {Name: "gopkg.in/src-d/go-license-detector.v2", License: "Apache-2.0"}, + {Name: "github.com/hashicorp/multierror-go", License: "MPL-2.0"}, + }, + DependencyBlob: "gopkg.in/src-d/go-license-detector.v2 Apache-2.0\ngithub.com/hashicorp/multierror-go MPL-2.0\n", + }, + wantOut: fooCompanyOut, + }, + { + name: "parses go.mod with two dependencies and one of them has no matches", + args: args{params: GenerateNoticeParams{ + GoModFile: "testfiles/twodeps.mod", + AnalyseFunc: genAnalyseFunc([]licensedb.Result{ + { + Arg: "github.com/hashicorp/multierror-go", + ErrStr: "no license file was found", + }, + { + Arg: "gopkg.in/src-d/go-license-detector.v2", + Matches: []licensedb.Match{{License: "Apache-2.0"}}, + }, + }), + Project: "somedeps", + }}, + want: Notice{Project: "somedeps", ProjectYears: "2019", Dependencies: []Dependency{ + {Name: "gopkg.in/src-d/go-license-detector.v2", License: "Apache-2.0"}, + {Name: "github.com/hashicorp/multierror-go", License: "no license file was found"}, + }}, + }, + { + name: "fails on invalid parameters", + args: args{params: GenerateNoticeParams{}}, + err: &multierror.Error{Errors: []error{ + errors.New("notice: missing file path"), + errors.New("notice: missing AnalyseFunc"), + errors.New("notice: missing project name"), + }}, + }, + { + name: "fails parsing gomod when the format is wrong", + args: args{params: GenerateNoticeParams{ + GoModFile: "notice.go", + AnalyseFunc: genAnalyseFunc(nil), + Project: "some", + }}, + want: Notice{Project: "some", ProjectYears: "2019"}, + err: errors.New("notice.go:41:42: unexpected newline in string"), + }, + { + name: "parses go.mod but has no dependencies", + args: args{params: GenerateNoticeParams{ + GoModFile: "testfiles/nodeps.mod", + AnalyseFunc: genAnalyseFunc(nil), + Project: "nodeps", + }}, + want: Notice{Project: "nodeps", ProjectYears: "2019"}, + err: errors.New("modfile has no dependencies to generate notice"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf = new(bytes.Buffer) + if tt.args.withBuffer { + tt.args.params.Writer = buf + } + + got, err := GenerateNotice(tt.args.params) + if !reflect.DeepEqual(err, tt.err) { + t.Errorf("GenerateNotice() error = %v, wantErr %v", err, tt.err) + return + } + + if buf.String() != tt.wantOut { + t.Errorf("GenerateNotice() output = %v, wantOut %v", buf.String(), tt.wantOut) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GenerateNotice() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/licensing/testfiles/nodeps.mod b/licensing/testfiles/nodeps.mod new file mode 100644 index 0000000..4d03ff6 --- /dev/null +++ b/licensing/testfiles/nodeps.mod @@ -0,0 +1,3 @@ +module github.com/elastic/go-licenser/licensing + +go 1.12 diff --git a/licensing/testfiles/twodeps.mod b/licensing/testfiles/twodeps.mod new file mode 100644 index 0000000..8f0f636 --- /dev/null +++ b/licensing/testfiles/twodeps.mod @@ -0,0 +1,8 @@ +module github.com/elastic/go-licenser/licensing + +go 1.12 + +require ( + github.com/hashicorp/go-multierror v1.0.0 + gopkg.in/src-d/go-license-detector.v2 v2.0.1 +) \ No newline at end of file diff --git a/main.go b/main.go index dd35fa8..52ba5ac 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/elastic/go-licenser/licensing" + "gopkg.in/src-d/go-license-detector.v2/licensedb" ) const ( @@ -45,6 +46,8 @@ const ( exitFailedToOpenWalkFile errFailedRewrittingFile errUnknownLicense + errOpenFileFailed + errGenerateNoticeFailed ) var usageText = ` @@ -53,6 +56,9 @@ Usage: go-licenser [flags] [path] go-licenser walks the specified path recursiely and appends a license Header if the current header doesn't match the one found in the file. + Using the -notice flag a compiled list of the project's dependencies and licenses is compiled + after the "go.mod" file is inspected. If the dependencies aren't found locally, it will fail. + Options: `[1:] @@ -103,6 +109,11 @@ var Headers = map[string][]string{ var ( dryRun bool showVersion bool + generateNotice bool + noticeYear string + noticeFile string + noticeHeader string + noticeProject string extension string args []string license string @@ -111,6 +122,22 @@ var ( defaultExludedDirs = []string{"vendor", ".git"} ) +type runParams struct { + args []string + license string + licensor string + exclude []string + ext string + dry bool + notice bool + noticeYear string + noticeFile string + noticeHeader string + noticeProject string + out io.Writer + analyseFunc func(args ...string) []licensedb.Result +} + type sliceFlag []string func (f *sliceFlag) String() string { @@ -130,6 +157,11 @@ func init() { flag.Var(&exclude, "exclude", `path to exclude (can be specified multiple times).`) flag.BoolVar(&dryRun, "d", false, `skips rewriting files and returns exitcode 1 if any discrepancies are found.`) flag.BoolVar(&showVersion, "version", false, `prints out the binary version.`) + flag.BoolVar(&generateNotice, "notice", false, `generates a NOTICE (use -notice-file to change it) file on the folder where it's being run.`) + flag.StringVar(¬iceYear, "notice-year", "", `specifies the start of the project so the notice file reflects it.`) + flag.StringVar(¬iceFile, "notice-file", "NOTICE", `specifies the file where to write the license notice.`) + flag.StringVar(¬iceHeader, "notice-header", "", `specifies the notice header Go Template.`) + flag.StringVar(¬iceProject, "notice-project-name", "", `specifies the notice project name at the top of the Go Template (defaults to folder name).`) flag.StringVar(&extension, "ext", defaultExt, "sets the file extension to scan for.") flag.StringVar(&license, "license", defaultLicense, "sets the license type to check: ASL2, Elastic, Cloud") flag.StringVar(&licensor, "licensor", defaultLicensor, "sets the name of the licensor") @@ -144,7 +176,21 @@ func main() { return } - err := run(args, license, licensor, exclude, extension, dryRun, os.Stdout) + err := run(runParams{ + args: args, + license: license, + licensor: licensor, + exclude: exclude, + ext: extension, + dry: dryRun, + notice: generateNotice, + noticeYear: noticeYear, + noticeFile: noticeFile, + noticeHeader: noticeHeader, + noticeProject: noticeProject, + out: os.Stdout, + analyseFunc: licensedb.Analyse, + }) if err != nil && err.Error() != "" { fmt.Fprint(os.Stderr, err) } @@ -152,31 +198,46 @@ func main() { os.Exit(Code(err)) } -func run(args []string, license, licensor string, exclude []string, ext string, dry bool, out io.Writer) error { - header, ok := Headers[license] +func run(params runParams) error { + header, ok := Headers[params.license] if !ok { - return &Error{err: fmt.Errorf("unknown license: %s", license), code: errUnknownLicense} + return &Error{err: fmt.Errorf("unknown license: %s", params.license), code: errUnknownLicense} } var headerBytes []byte for i, line := range header { if strings.Contains(line, "%s") { - header[i] = fmt.Sprintf(line, licensor) + header[i] = fmt.Sprintf(line, params.licensor) } headerBytes = append(headerBytes, []byte(header[i])...) headerBytes = append(headerBytes, []byte("\n")...) } var path = defaultPath - if len(args) > 0 { - path = args[0] + if len(params.args) > 0 { + path = params.args[0] } if _, err := os.Stat(path); err != nil { return &Error{err: err, code: exitFailedToStatTree} } - return walk(path, ext, license, headerBytes, exclude, dry, out) + var walkErr error + if err := walk(path, params.ext, params.license, headerBytes, + params.exclude, params.dry, params.out, + ); err != nil { + walkErr = err + } + + if !params.notice { + return walkErr + } + + if err := doNotice(path, params); err != nil { + return err + } + + return walkErr } func reportFile(out io.Writer, f string) { @@ -256,3 +317,14 @@ func usageFlag() { fmt.Fprintf(os.Stderr, usageText) flag.PrintDefaults() } + +func openTruncateFile(p string) (*os.File, error) { + f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) + if err != nil { + return nil, err + } + + f.Truncate(0) + f.Seek(0, 0) + return f, nil +} diff --git a/main_test.go b/main_test.go index c8bae1a..a7620ce 100644 --- a/main_test.go +++ b/main_test.go @@ -32,6 +32,8 @@ import ( "strings" "syscall" "testing" + + "gopkg.in/src-d/go-license-detector.v2/licensedb" ) const fixtures = "fixtures" @@ -104,26 +106,37 @@ func dcopy(src, dest string, info os.FileInfo) error { return nil } +var goModAnalyseFunc = genAnalyseFunc([]licensedb.Result{ + { + Arg: "github.com/hashicorp/multierror-go", + Matches: []licensedb.Match{{License: "MPL-2.0"}}, + }, + { + Arg: "github.com/sirkon/goproxy", + Matches: []licensedb.Match{{License: "MIT"}}, + }, + { + Arg: "gopkg.in/src-d/go-license-detector.v2", + Matches: []licensedb.Match{{License: "Apache-2.0"}}, + }, +}) + +func genAnalyseFunc(r []licensedb.Result) func(args ...string) []licensedb.Result { + return func(args ...string) []licensedb.Result { return r } +} + func Test_run(t *testing.T) { - type args struct { - args []string - license string - licensor string - exclude []string - ext string - dry bool - } tests := []struct { name string - args args + args runParams want int err error wantOutput string wantGolden bool }{ { - name: "Run a diff prints a list of files that need the license header", - args: args{ + name: "Run a diff prints a list of files that need the default license header", + args: runParams{ args: []string{"testdata"}, license: defaultLicense, licensor: defaultLicensor, @@ -142,11 +155,52 @@ testdata/multilevel/sublevel/partial.go: is missing the license header testdata/singlelevel/doc.go: is missing the license header testdata/singlelevel/main.go: is missing the license header testdata/singlelevel/wrapper.go: is missing the license header +`[1:], + }, + { + name: "Run a diff prints a list of files that need the default license header and notice", + args: runParams{ + args: []string{"testdata"}, + license: defaultLicense, + licensor: defaultLicensor, + exclude: []string{"excludedpath", "x-pack", "cloud"}, + ext: defaultExt, + dry: true, + notice: true, + noticeProject: "SomeProject", + analyseFunc: goModAnalyseFunc, + }, + want: 1, + err: &Error{code: 1}, + wantOutput: ` +testdata/multilevel/doc.go: is missing the license header +testdata/multilevel/main.go: is missing the license header +testdata/multilevel/sublevel/autogen.go: is missing the license header +testdata/multilevel/sublevel/doc.go: is missing the license header +testdata/multilevel/sublevel/partial.go: is missing the license header +testdata/singlelevel/doc.go: is missing the license header +testdata/singlelevel/main.go: is missing the license header +testdata/singlelevel/wrapper.go: is missing the license header +Dumping NOTICE to output... + +SomeProject +Copyright 2019 Elasticsearch B.V. + +This product includes software developed at Elasticsearch B.V. and +third-party software developed by the licenses listed below. + +========================================================================= + +gopkg.in/src-d/go-license-detector.v2 Apache-2.0 +github.com/sirkon/goproxy MIT +github.com/hashicorp/multierror-go MPL-2.0 + +========================================================================= `[1:], }, { name: "Run a diff prints a list of files that need the Elastic license header", - args: args{ + args: runParams{ args: []string{"testdata"}, license: "Elastic", licensor: defaultLicensor, @@ -173,7 +227,7 @@ testdata/x-pack/wrong.go: is missing the license header }, { name: "Run a diff prints a list of files that need the Cloud license header", - args: args{ + args: runParams{ args: []string{"testdata"}, license: "Cloud", licensor: defaultLicensor, @@ -200,7 +254,7 @@ testdata/x-pack/wrong.go: is missing the license header }, { name: "Run against an unexisting dir fails", - args: args{ + args: runParams{ args: []string{"ignore"}, license: defaultLicense, licensor: defaultLicensor, @@ -212,7 +266,7 @@ testdata/x-pack/wrong.go: is missing the license header }, { name: "Unknown license fails", - args: args{ + args: runParams{ args: []string{"ignore"}, license: "foo", licensor: defaultLicensor, @@ -223,16 +277,20 @@ testdata/x-pack/wrong.go: is missing the license header err: &Error{err: errors.New("unknown license: foo"), code: 7}, }, { - name: "Run with default mode rewrites the source files", - args: args{ - args: []string{"testdata"}, - license: defaultLicense, - licensor: defaultLicensor, - exclude: []string{"excludedpath", "x-pack", "cloud"}, - ext: defaultExt, - dry: false, + name: "Run with default mode rewrites the source files and notice", + args: runParams{ + args: []string{"testdata"}, + license: defaultLicense, + licensor: defaultLicensor, + exclude: []string{"excludedpath", "x-pack", "cloud"}, + ext: defaultExt, + dry: false, + notice: true, + noticeProject: "SomeProject", + analyseFunc: goModAnalyseFunc, }, want: 0, + wantOutput: "Generating NOTICE file...\n\n", wantGolden: true, }, } @@ -243,7 +301,8 @@ testdata/x-pack/wrong.go: is missing the license header } var buf = new(bytes.Buffer) - var err = run(tt.args.args, tt.args.license, tt.args.licensor, tt.args.exclude, tt.args.ext, tt.args.dry, buf) + tt.args.out = buf + var err = run(tt.args) if !reflect.DeepEqual(err, tt.err) { t.Errorf("run() error = %v, wantErr %v", err, tt.err) return @@ -255,7 +314,12 @@ testdata/x-pack/wrong.go: is missing the license header } var gotOutput = buf.String() - tt.wantOutput = filepath.FromSlash(tt.wantOutput) + if so := strings.Split(tt.wantOutput, "Dumping"); len(so) > 1 { + tt.wantOutput = filepath.FromSlash(so[0]) + "Dumping" + so[1] + } else { + tt.wantOutput = filepath.FromSlash(tt.wantOutput) + } + if gotOutput != tt.wantOutput { t.Errorf("Output = \n%v\n want \n%v", gotOutput, tt.wantOutput) } @@ -263,7 +327,9 @@ testdata/x-pack/wrong.go: is missing the license header if tt.wantGolden { if *update { copyFixtures(t, "golden") - if err := run([]string{"golden"}, tt.args.license, tt.args.licensor, tt.args.exclude, tt.args.ext, tt.args.dry, buf); err != nil { + params := tt.args + params.args = []string{"golden"} + if err := run(params); err != nil { t.Fatal(err) } } diff --git a/notice.go b/notice.go new file mode 100644 index 0000000..da4cd87 --- /dev/null +++ b/notice.go @@ -0,0 +1,87 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/elastic/go-licenser/licensing" +) + +func doNotice(path string, params runParams) error { + // TODO: potentially use git to extract the date. + var startDate, _ = time.Parse("2006", params.noticeYear) + + // When the noticeYear hasn't been specified, it tries to autodiscover + // which year the project started by gathering the oldest modification + // timestamp in files by walking the directory. + if params.noticeYear == "" { + startDate = time.Now() + if err := filepath.Walk(path, + func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || strings.Contains(p, ".git") { + return nil + } + if fileMod := info.ModTime(); fileMod.Before(startDate) { + startDate = fileMod + } + return nil + }); err != nil { + return err + } + } + + var noticeWriter = params.out + var noticeMessage = "Dumping NOTICE to output...\n\n" + if !params.dry { + var noticeFilePath = filepath.Join(path, noticeFile) + f, err := openTruncateFile(noticeFilePath) + if err != nil { + return &Error{err: err, code: errOpenFileFailed} + } + defer f.Close() + noticeWriter = f + noticeMessage = "Generating NOTICE file...\n\n" + } + fmt.Fprintf(params.out, noticeMessage) + + if params.noticeProject == "" { + absPath, _ := filepath.Abs(path) + params.noticeProject = filepath.Base(absPath) + } + if _, err := licensing.GenerateNotice(licensing.GenerateNoticeParams{ + GoModFile: filepath.Join(path, "go.mod"), + Writer: noticeWriter, + Project: params.noticeProject, + Licensor: licensor, + StartYear: startDate.Year(), + NoticeHeader: params.noticeHeader, + AnalyseFunc: params.analyseFunc, + }); err != nil { + return &Error{err: err, code: errGenerateNoticeFailed} + } + + return nil +}