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 +}