From 19884519444c8bba5a7eda437db5aed93405c51c Mon Sep 17 00:00:00 2001 From: Axel Christ Date: Wed, 6 Apr 2022 15:38:28 +0200 Subject: [PATCH] Initial commit --- .github/dependabot.yml | 17 ++ .github/workflows/golangci-lint.yml | 20 ++ .github/workflows/release.yml | 31 +++ .github/workflows/test.yml | 16 ++ .gitignore | 33 +++ .golangci.yaml | 20 ++ .goreleaser.yaml | 25 ++ LICENSE | 201 +++++++++++++++ Makefile | 77 ++++++ README.md | 36 +++ bin/.gitkeep | 0 go.mod | 21 ++ go.sum | 114 +++++++++ hack/boilerplate.go.txt | 15 ++ hack/tools.go | 25 ++ internal/dstGopath/pkg | 1 + internal/internal.go | 350 ++++++++++++++++++++++++++ internal/internal_suite_test.go | 27 ++ internal/internal_test.go | 258 +++++++++++++++++++ internal/testdata/modules.json.stream | 11 + main.go | 62 +++++ 21 files changed, 1360 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .goreleaser.yaml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bin/.gitkeep create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/boilerplate.go.txt create mode 100644 hack/tools.go create mode 120000 internal/dstGopath/pkg create mode 100644 internal/internal.go create mode 100644 internal/internal_suite_test.go create mode 100644 internal/internal_test.go create mode 100644 internal/testdata/modules.json.stream create mode 100644 main.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..62b6efa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +--- +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + reviewers: + - "onmetal/onmetal-api-maintainers" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + reviewers: + - "onmetal/onmetal-api-maintainers" diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..d5befe5 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,20 @@ +name: Lint Golang Codebase + +on: + pull_request: + paths-ignore: + - 'docs/**' + - '**/*.md' +jobs: + golangci: + name: lint + runs-on: self-hosted + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v2 + with: + go-version: '1.17' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.43.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..972d975 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: goreleaser + +on: + pull_request: + push: + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Fetch all tags + run: git fetch --force --tags + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9e7e091 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: test + +on: + pull_request: + push: + +jobs: + checks: + name: run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v2 + with: + go-version: '1.17' + - run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae25abf --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* +vendor/ + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +.vscode/ + +# Utilities +testbin/ + +# Goreleaser +dist/ + diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..8bf4220 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,20 @@ +run: + timeout: 3m + +linters: + enable: + - revive + - ineffassign + - misspell + - goimports + +severity: + default-severity: error + +linters-settings: + revive: + severity: error + rules: + - name: exported + - name: if-return + disabled: true diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..3e58a05 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,25 @@ +project_name: github.com/onmetal/vgopath +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7170e85 --- /dev/null +++ b/Makefile @@ -0,0 +1,77 @@ +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +all: build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: lint +lint: + golangci-lint run ./... + +.PHONY: vet +vet: + go vet ./... + +.PHONY: addlicense +addlicense: ## Add license headers to all go files. + find . -name '*.go' -exec go run github.com/google/addlicense -c 'OnMetal authors' {} + + +.PHONY: checklicense +checklicense: ## Check that every file has a license header present. + find . -name '*.go' -exec go run github.com/google/addlicense -check -c 'OnMetal authors' {} + + +.PHONY: check +check: addlicense lint test # Generate manifests, code, lint, add licenses, test + +.PHONY: test +test: fmt vet ## Run tests. + go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: fmt vet ## Build manager binary. + go build -o bin/vgopath main.go + +.PHONY: install +install: + go install . + +.PHONY: run +run: fmt lint ## Run a controller from your host. + go run ./main.go + +.PHONY: release +release: fmt lint + goreleaser release --snapshot --rm-dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa1e5d4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# vgopath + +`vgopath` is a tool for module-enabled projects to set up a 'virtual' GOPATH for +legacy tools to run with (`kubernetes/code-generator` I'm looking at you...). + +## Installation + +The simplest way to install `vgopath` is by running + +```shell +go install github.com/onmetal/vgopath@latest +``` + +## Usage + +`vgopath` has to be run from the module-enabled project root. It requires a +target directory to construct the virtual GOPATH. + +Example usage could look like this: + +```shell +# Create the target directory +mkdir -p my-vgopath + +# Do the linking in my-vgopath +vgopath my-vgopath +``` + +Once done, the structure will look something like + +``` +my-vgopath +├── bin -> /bin +├── pkg -> /pkg +└── src -> various subdirectories +``` diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..766cfbd --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/onmetal/vgopath + +go 1.17 + +require ( + github.com/google/addlicense v1.0.0 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.19.0 +) + +require ( + github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/nxadm/tail v1.4.8 // indirect + golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0170886 --- /dev/null +++ b/go.sum @@ -0,0 +1,114 @@ +github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA= +github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/addlicense v1.0.0 h1:cqvo5suPWlsk6r6o42Fs2K66xYCl2tnhVPUYoP3EnO4= +github.com/google/addlicense v1.0.0/go.mod h1:Sm/DHu7Jk+T5miFHHehdIjbi4M5+dJDRS3Cq0rncIxA= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..e5a66c4 --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 by the OnMetal authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ \ No newline at end of file diff --git a/hack/tools.go b/hack/tools.go new file mode 100644 index 0000000..1758eb0 --- /dev/null +++ b/hack/tools.go @@ -0,0 +1,25 @@ +// Copyright 2021 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tools + +//go:build tools +// +build tools + +package tools + +import ( + // Use addlicense for adding license headers. + _ "github.com/google/addlicense" +) diff --git a/internal/dstGopath/pkg b/internal/dstGopath/pkg new file mode 120000 index 0000000..7c2c426 --- /dev/null +++ b/internal/dstGopath/pkg @@ -0,0 +1 @@ +srcGopath/pkg \ No newline at end of file diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 0000000..0162328 --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,350 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "encoding/json" + "fmt" + "go/build" + "io" + "os" + "os/exec" + "path" + "path/filepath" + "sort" + "strings" + "time" +) + +type Node struct { + Segment string + Module *Module + Children []Node +} + +func insertModuleInNode(node *Node, mod Module, relativeSegments []string) error { + if len(relativeSegments) == 0 { + if node.Module != nil { + return fmt.Errorf("cannot insert module %s into node %s: module %s already exists", mod.Path, node.Segment, node.Module.Path) + } + + node.Module = &mod + return nil + } + + var ( + idx = -1 + segment = relativeSegments[0] + ) + for i, child := range node.Children { + if child.Segment == segment { + idx = i + break + } + } + + var child *Node + if idx == -1 { + child = &Node{Segment: segment} + } else { + child = &node.Children[idx] + } + + if err := insertModuleInNode(child, mod, relativeSegments[1:]); err != nil { + return err + } + + if idx == -1 { + node.Children = append(node.Children, *child) + } + + return nil +} + +func BuildModuleNodes(modules []Module) ([]Node, error) { + sort.Slice(modules, func(i, j int) bool { return modules[i].Path < modules[j].Path }) + nodeByRootSegment := make(map[string]*Node) + + for _, module := range modules { + if module.Path == "" { + return nil, fmt.Errorf("invalid empty module path") + } + + segments := strings.Split(module.Path, "/") + + rootSegment := segments[0] + node, ok := nodeByRootSegment[rootSegment] + if !ok { + node = &Node{Segment: rootSegment} + nodeByRootSegment[rootSegment] = node + } + + if err := insertModuleInNode(node, module, segments[1:]); err != nil { + return nil, err + } + } + + res := make([]Node, 0, len(nodeByRootSegment)) + for _, node := range nodeByRootSegment { + res = append(res, *node) + } + return res, nil +} + +type Module struct { + Path string + Dir string +} + +type moduleReader struct { + cmd *exec.Cmd + waitDone chan struct{} + waitErr error + stdout io.ReadCloser +} + +func StartModuleReader() (io.ReadCloser, error) { + r := &moduleReader{} + + cmd := exec.Command("go", "list", "-m", "-json", "all") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + if err := cmd.Start(); err != nil { + return nil, err + } + + r.waitDone = make(chan struct{}) + go func() { + defer close(r.waitDone) + r.waitErr = cmd.Wait() + }() + + return &moduleReader{ + cmd: cmd, + stdout: stdout, + }, nil +} + +func (r *moduleReader) Read(p []byte) (n int, err error) { + return r.stdout.Read(p) +} + +func (r *moduleReader) Close() error { + select { + case <-r.waitDone: + return r.waitErr + default: + } + + timer := time.NewTimer(3 * time.Second) + defer timer.Stop() + + select { + case <-timer.C: + return fmt.Errorf("error waiting for command to be completed") + case <-r.waitDone: + return r.waitErr + } +} + +func ParseModules(r io.Reader) ([]Module, error) { + var ( + mods []Module + dec = json.NewDecoder(r) + ) + + for { + var mod Module + if err := dec.Decode(&mod); err != nil { + if err == io.EOF { + break + } + return nil, err + } + + // Don't include indirect modules without directory. + if mod.Dir == "" { + continue + } + mods = append(mods, mod) + } + return mods, nil +} + +func ReadModules() ([]Module, error) { + rc, err := StartModuleReader() + if err != nil { + return nil, err + } + defer func() { _ = rc.Close() }() + + return ParseModules(rc) +} + +type Options struct { + SkipGoBin bool + SkipGoSrc bool + SkipGoPkg bool +} + +func Run(dstDir string, opts Options) error { + if !opts.SkipGoSrc { + if err := LinkGoSrc(dstDir); err != nil { + return fmt.Errorf("error linking GOPATH/src: %w", err) + } + } + + if !opts.SkipGoBin { + if err := LinkGoBin(dstDir); err != nil { + return fmt.Errorf("error linking GOPATH/bin: %w", err) + } + } + + if !opts.SkipGoPkg { + if err := LinkGoPkg(dstDir); err != nil { + return fmt.Errorf("error linking GOPATH/pkg: %w", err) + } + } + + return nil +} + +func LinkGoBin(dstDir string) error { + dstGoBinDir := filepath.Join(dstDir, "bin") + if err := os.RemoveAll(dstGoBinDir); err != nil { + return err + } + + srcGoBinDir := os.Getenv("GOBIN") + if srcGoBinDir == "" { + srcGoBinDir = filepath.Join(build.Default.GOPATH, "bin") + } + + if err := os.Symlink(srcGoBinDir, dstGoBinDir); err != nil { + return err + } + return nil +} + +func LinkGoPkg(dstDir string) error { + dstGoPkgDir := filepath.Join(dstDir, "pkg") + if err := os.RemoveAll(dstGoPkgDir); err != nil { + return err + } + + if err := os.Symlink(filepath.Join(build.Default.GOPATH, "pkg"), dstGoPkgDir); err != nil { + return err + } + return nil +} + +func LinkGoSrc(dstDir string) error { + mods, err := ReadModules() + if err != nil { + return fmt.Errorf("error reading modules: %w", err) + } + + nodes, err := BuildModuleNodes(mods) + if err != nil { + return fmt.Errorf("error building module tree: %w", err) + } + + dstGoSrcDir := filepath.Join(dstDir, "src") + if err := os.RemoveAll(dstGoSrcDir); err != nil { + return err + } + + if err := os.Mkdir(dstGoSrcDir, 0777); err != nil { + return err + } + + if err := LinkNodes(dstGoSrcDir, nodes); err != nil { + return err + } + return nil +} + +type linkNodeError struct { + path string + err error +} + +func (l *linkNodeError) Error() string { + return fmt.Sprintf("[path %s]: %v", l.path, l.err) +} + +func joinLinkNodeError(node Node, err error) error { + if linkNodeErr, ok := err.(*linkNodeError); ok { + return &linkNodeError{ + path: path.Join(node.Segment, linkNodeErr.path), + err: linkNodeErr.err, + } + } + return &linkNodeError{ + path: node.Segment, + err: err, + } +} + +func LinkNodes(dir string, nodes []Node) error { + for _, node := range nodes { + if err := linkNode(dir, node); err != nil { + return joinLinkNodeError(node, err) + } + } + return nil +} + +func linkNode(dir string, node Node) error { + dstDir := filepath.Join(dir, node.Segment) + + // If the node specifies a module and no children are present, we can take optimize and directly + // symlink the module directory to the destination directory. + if node.Module != nil && len(node.Children) == 0 { + srcDir := node.Module.Dir + + if err := os.Symlink(srcDir, dstDir); err != nil { + return fmt.Errorf("error symlinking node: %w", err) + } + } + + if err := os.RemoveAll(dstDir); err != nil { + return err + } + + if err := os.Mkdir(dstDir, 0777); err != nil { + return err + } + + if node.Module != nil { + srcDir := node.Module.Dir + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + if err := os.Symlink(srcPath, dstPath); err != nil { + return fmt.Errorf("error symlinking entry %s to %s: %w", srcPath, dstPath, err) + } + } + } + return LinkNodes(dstDir, node.Children) +} diff --git a/internal/internal_suite_test.go b/internal/internal_suite_test.go new file mode 100644 index 0000000..585be80 --- /dev/null +++ b/internal/internal_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestInternal(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Internal Suite") +} diff --git a/internal/internal_test.go b/internal/internal_test.go new file mode 100644 index 0000000..92ab500 --- /dev/null +++ b/internal/internal_test.go @@ -0,0 +1,258 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal_test + +import ( + "bytes" + "fmt" + "go/build" + "os" + "path/filepath" + + . "github.com/onmetal/vgopath/internal" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" +) + +var _ = Describe("Internal", func() { + var ( + tmpDir string + moduleA, moduleB, moduleB1, moduleB11, moduleB2, moduleC Module + ) + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + moduleA = Module{ + Path: "a", + Dir: "/tmp/a", + } + moduleB = Module{ + Path: "example.org/b", + Dir: "/tmp/example.org/b", + } + moduleB1 = Module{ + Path: "example.org/b/1", + Dir: "/tmp/example.org/b/1", + } + moduleB11 = Module{ + Path: "example.org/b/1/1", + Dir: "/tmp/example.org/b/1/1", + } + moduleB2 = Module{ + Path: "example.org/b/2", + Dir: "/tmp/example.org/b/2", + } + moduleC = Module{ + Path: "example.org/user/c", + Dir: "/tmp/example.org/user/c", + } + }) + + Describe("BuildModuleNodes", func() { + It("should correctly build the nodes", func() { + nodes, err := BuildModuleNodes([]Module{moduleA, moduleB, moduleB1, moduleB11, moduleB2, moduleC}) + Expect(err).NotTo(HaveOccurred()) + Expect(nodes).To(ConsistOf( + Node{ + Segment: "a", + Module: &moduleA, + }, + Node{ + Segment: "example.org", + Children: []Node{ + { + Segment: "b", + Module: &moduleB, + Children: []Node{ + { + Segment: "1", + Module: &moduleB1, + Children: []Node{ + { + Segment: "1", + Module: &moduleB11, + }, + }, + }, + { + Segment: "2", + Module: &moduleB2, + }, + }, + }, + { + Segment: "user", + Children: []Node{ + { + Segment: "c", + Module: &moduleC, + }, + }, + }, + }, + }, + )) + }) + + It("should error on invalid module paths", func() { + _, err := BuildModuleNodes([]Module{{Path: ""}}) + Expect(err).To(HaveOccurred()) + }) + + It("should error if there are modules pointing to the same path", func() { + _, err := BuildModuleNodes([]Module{{Path: "foo"}, {Path: "foo"}}) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("ParseModules", func() { + It("should correctly parse the modules", func() { + data, err := os.ReadFile(filepath.Join("testdata", "modules.json.stream")) + Expect(err).NotTo(HaveOccurred()) + + mods, err := ParseModules(bytes.NewReader(data)) + Expect(err).NotTo(HaveOccurred()) + + Expect(mods).To(Equal([]Module{moduleA, moduleB})) + }) + }) + + Context("Link", func() { + var ( + srcGopathDir string + dstGopathDir string + ) + BeforeEach(func() { + srcGopathDir = filepath.Join(tmpDir, "srcGopath") + dstGopathDir = filepath.Join(tmpDir, "dstGopath") + + Expect(os.MkdirAll(srcGopathDir, 0777)).To(Succeed()) + Expect(os.MkdirAll(dstGopathDir, 0777)).To(Succeed()) + }) + + Describe("LinkGoBin", func() { + var ( + srcGoBinDir string + dstGoBinDir string + ) + BeforeEach(func() { + srcGoBinDir = filepath.Join(srcGopathDir, "bin") + Expect(os.MkdirAll(srcGoBinDir, 0777)).To(Succeed()) + + dstGoBinDir = filepath.Join(dstGopathDir, "bin") + Expect(os.MkdirAll(dstGopathDir, 0777)).To(Succeed()) + }) + + It("should correctly link go bin", func() { + defer setEnvAndRevert("GOBIN", "")() + defer setAndRevert(&build.Default.GOPATH, srcGopathDir)() + + Expect(LinkGoBin(dstGopathDir)).To(Succeed()) + Expect(dstGoBinDir).To(BeASymlinkTo(srcGoBinDir)) + }) + + It("should correctly link go bin if GOBIN is set", func() { + defer setEnvAndRevert("GOBIN", srcGoBinDir)() + + Expect(LinkGoBin(dstGopathDir)).To(Succeed()) + Expect(dstGoBinDir).To(BeASymlinkTo(srcGoBinDir)) + }) + }) + + Describe("LinkGoPkg", func() { + var ( + srcGoPkgDir string + dstGoPkgDir string + ) + BeforeEach(func() { + srcGoPkgDir = filepath.Join(srcGopathDir, "pkg") + Expect(os.MkdirAll(srcGoPkgDir, 0777)).To(Succeed()) + + dstGoPkgDir = filepath.Join(dstGopathDir, "pkg") + Expect(os.MkdirAll(dstGopathDir, 0777)).To(Succeed()) + }) + + It("should correctly link go pkg", func() { + defer setAndRevert(&build.Default.GOPATH, srcGopathDir)() + + Expect(LinkGoPkg(dstGopathDir)).To(Succeed()) + Expect(dstGoPkgDir).To(BeASymlinkTo(srcGoPkgDir)) + }) + }) + }) +}) + +func BeASymlinkTo(filename string) types.GomegaMatcher { + return &beASymlinkToMatcher{filename} +} + +type beASymlinkToMatcher struct { + filename string +} + +func (m *beASymlinkToMatcher) Match(actual interface{}) (success bool, err error) { + actualFilename, ok := actual.(string) + if !ok { + return false, fmt.Errorf("IsSymlinkTo expects a filename string") + } + + actualStat, err := os.Lstat(actualFilename) + if err != nil { + return false, err + } + + if (actualStat.Mode() & os.ModeSymlink) != os.ModeSymlink { + return false, nil + } + + tgt, err := os.Readlink(actualFilename) + if err != nil { + return false, err + } + + return tgt == m.filename, nil +} + +func (m *beASymlinkToMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%v\nto be a symlink to\n\t%s", actual, m.filename) +} + +func (m *beASymlinkToMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%v\nnot to be a symlink to\n\t%s", actual, m.filename) +} + +func setEnvAndRevert(key, value string) func() { + oldValue := os.Getenv(key) + if value == "" { + _ = os.Unsetenv(key) + } else { + _ = os.Setenv(key, value) + } + return func() { + if oldValue == "" { + _ = os.Unsetenv(key) + } else { + _ = os.Setenv(key, oldValue) + } + } +} + +func setAndRevert(pointerToString *string, newValue string) func() { + oldValue := *pointerToString + *pointerToString = newValue + return func() { + *pointerToString = oldValue + } +} diff --git a/internal/testdata/modules.json.stream b/internal/testdata/modules.json.stream new file mode 100644 index 0000000..c8fb69e --- /dev/null +++ b/internal/testdata/modules.json.stream @@ -0,0 +1,11 @@ +{ + "Path": "a", + "Dir": "/tmp/a" +} +{ + "Path": "example.org/b", + "Dir": "/tmp/example.org/b" +} +{ + "Path": "example.org/d" +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3dd951b --- /dev/null +++ b/main.go @@ -0,0 +1,62 @@ +// Copyright 2022 OnMetal authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/onmetal/vgopath/internal" +) + +func Usage() { + _, _ = fmt.Fprint(flag.CommandLine.Output(), `vgopath - Virtual gopath + +Usage: + vgopath [opts] + +Create a 'virtual' GOPATH at the specified directory. +Has to be run from a go module. + +vgopath will setup a GOPATH folder structure, ensuring that any tool used +to the traditional setup will function as normal. + +The current module will be mirrored to where its go.mod path (the line +after 'module') points at. + +`) + flag.PrintDefaults() +} + +func main() { + var opts internal.Options + flag.BoolVar(&opts.SkipGoPkg, "skip-go-pkg", opts.SkipGoPkg, "Whether to skip mirroring $GOPATH/pkg") + flag.BoolVar(&opts.SkipGoBin, "skip-go-bin", opts.SkipGoBin, "Whether to skip mirroring $GOBIN") + flag.BoolVar(&opts.SkipGoSrc, "skip-go-src", opts.SkipGoSrc, "Whether to skip mirroring modules as src") + flag.Usage = Usage + + flag.Parse() + dstDir := flag.Arg(0) + if dstDir == "" { + flag.Usage() + os.Exit(1) + } + + if err := internal.Run(dstDir, opts); err != nil { + fmt.Printf("Error running vgopath:\n%v", err) + os.Exit(1) + } +}