From 33bdccc45c1e0ee613484f1f282337d3e9a7ea2f Mon Sep 17 00:00:00 2001 From: Artem Chernyshev Date: Thu, 18 May 2023 22:46:39 +0300 Subject: [PATCH] feat: support nested Go projects Generate boilerplate for: - unit-tests. - linters. - codecoverage. - build executables from nested projects. Signed-off-by: Artem Chernyshev --- .drone.yml | 2 +- .golangci.yml | 6 +- Dockerfile | 27 ++- Makefile | 18 +- internal/output/drone/step.go | 11 +- internal/output/golangci/golangci.go | 42 ++-- internal/output/makefile/group.go | 8 + internal/output/makefile/makefile.go | 9 + internal/project/auto/golang.go | 231 ++++++++++++------- internal/project/golang/gen_name.go | 18 ++ internal/project/golang/generate.go | 4 +- internal/project/golang/gofumpt.go | 49 ++-- internal/project/golang/gofumpt_test.go | 2 - internal/project/golang/goimports.go | 41 ++-- internal/project/golang/goimports_test.go | 2 - internal/project/golang/golangcilint.go | 41 ++-- internal/project/golang/golangcilint_test.go | 2 - internal/project/golang/govulncheck.go | 31 +-- internal/project/golang/govulncheck_test.go | 2 - internal/project/golang/linters.go | 74 ++++++ internal/project/golang/linters_test.go | 18 ++ internal/project/golang/toolchain.go | 20 +- internal/project/golang/unit_tests.go | 33 +-- internal/project/meta/meta.go | 42 +++- internal/project/service/codecov.go | 15 +- 25 files changed, 491 insertions(+), 257 deletions(-) create mode 100644 internal/project/golang/gen_name.go create mode 100644 internal/project/golang/linters.go create mode 100644 internal/project/golang/linters_test.go diff --git a/.drone.yml b/.drone.yml index 9fa14ed..b22acc3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,7 +1,7 @@ --- # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-03-01T19:46:03Z by kres 636983d-dirty. +# Generated on 2023-05-18T19:10:42Z by kres 5240ae1-dirty. kind: pipeline type: kubernetes diff --git a/.golangci.yml b/.golangci.yml index 3cd4d55..e2e99a6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-02-02T11:37:12Z by kres 5686454-dirty. +# Generated on 2023-05-18T16:29:23Z by kres 5240ae1-dirty. # options for analysis running run: @@ -36,7 +36,7 @@ linters-settings: lines: 60 statements: 40 gci: - local-prefixes: github.com/siderolabs/kres + local-prefixes: github.com/siderolabs/kres/ gocognit: min-complexity: 30 ireturn: @@ -65,7 +65,7 @@ linters-settings: gofmt: simplify: true goimports: - local-prefixes: github.com/siderolabs/kres + local-prefixes: github.com/siderolabs/kres/ golint: min-confidence: 0.8 gomnd: diff --git a/Dockerfile b/Dockerfile index 27a2aa0..a97584a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-17T06:04:50Z by kres 0f0b8c2. +# Generated on 2023-05-19T11:21:06Z by kres 05a5352-dirty. ARG TOOLCHAIN @@ -32,26 +32,27 @@ ENV GO111MODULE on ARG CGO_ENABLED ENV CGO_ENABLED ${CGO_ENABLED} ENV GOPATH /go +ARG DEEPCOPY_VERSION +RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ + && mv /go/bin/deep-copy /bin/deep-copy ARG GOLANGCILINT_VERSION RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \ && mv /go/bin/golangci-lint /bin/golangci-lint -ARG GOFUMPT_VERSION -RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ - && mv /go/bin/gofumpt /bin/gofumpt RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/vuln/cmd/govulncheck@latest \ && mv /go/bin/govulncheck /bin/govulncheck ARG GOIMPORTS_VERSION RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} \ && mv /go/bin/goimports /bin/goimports -ARG DEEPCOPY_VERSION -RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ - && mv /go/bin/deep-copy /bin/deep-copy +ARG GOFUMPT_VERSION +RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ + && mv /go/bin/gofumpt /bin/gofumpt # tools and sources FROM tools AS base WORKDIR /src -COPY ./go.mod . -COPY ./go.sum . +COPY go.mod go.mod +COPY go.sum go.sum +RUN cd . RUN --mount=type=cache,target=/go/pkg go mod download RUN --mount=type=cache,target=/go/pkg go mod verify COPY ./cmd ./cmd @@ -75,25 +76,29 @@ RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is no # runs goimports FROM base AS lint-goimports -RUN FILES="$(goimports -l -local github.com/siderolabs/kres .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local github.com/siderolabs/kres .':\n${FILES}"; exit 1) +RUN FILES="$(goimports -l -local github.com/siderolabs/kres/ .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local github.com/siderolabs/kres/ .':\n${FILES}"; exit 1) # runs golangci-lint FROM base AS lint-golangci-lint +WORKDIR /src COPY .golangci.yml . ENV GOGC 50 RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/.cache/golangci-lint --mount=type=cache,target=/go/pkg golangci-lint run --config .golangci.yml # runs govulncheck FROM base AS lint-govulncheck +WORKDIR /src RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg govulncheck ./... # runs unit-tests with race detector FROM base AS unit-tests-race +WORKDIR /src ARG TESTPKGS RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp CGO_ENABLED=1 go test -v -race -count 1 ${TESTPKGS} # runs unit-tests FROM base AS unit-tests-run +WORKDIR /src ARG TESTPKGS RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS} @@ -101,7 +106,7 @@ FROM scratch AS kres-linux-amd64 COPY --from=kres-linux-amd64-build /kres-linux-amd64 /kres-linux-amd64 FROM scratch AS unit-tests -COPY --from=unit-tests-run /src/coverage.txt /coverage.txt +COPY --from=unit-tests-run /src/coverage.txt /coverage-unit-tests.txt FROM kres-linux-${TARGETARCH} AS kres diff --git a/Makefile b/Makefile index 449ea16..56649ab 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2023-05-11T04:45:18Z by kres 00c9f23. +# Generated on 2023-05-19T11:21:06Z by kres 05a5352-dirty. # common variables @@ -13,15 +13,15 @@ WITH_RACE ?= false REGISTRY ?= ghcr.io USERNAME ?= siderolabs REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) -GOLANGCILINT_VERSION ?= v1.52.2 -GOFUMPT_VERSION ?= v0.5.0 -GO_VERSION ?= 1.20 -GOIMPORTS_VERSION ?= v0.9.1 PROTOBUF_GO_VERSION ?= 1.28.1 GRPC_GO_VERSION ?= 1.3.0 GRPC_GATEWAY_VERSION ?= 2.15.2 VTPROTOBUF_VERSION ?= 0.4.0 DEEPCOPY_VERSION ?= v0.5.5 +GOLANGCILINT_VERSION ?= v1.52.2 +GOFUMPT_VERSION ?= v0.5.0 +GO_VERSION ?= 1.20 +GOIMPORTS_VERSION ?= v0.9.1 GO_BUILDFLAGS ?= GO_LDFLAGS ?= CGO_ENABLED ?= 0 @@ -49,14 +49,14 @@ COMMON_ARGS += --build-arg=TOOLCHAIN="$(TOOLCHAIN)" COMMON_ARGS += --build-arg=CGO_ENABLED="$(CGO_ENABLED)" COMMON_ARGS += --build-arg=GO_BUILDFLAGS="$(GO_BUILDFLAGS)" COMMON_ARGS += --build-arg=GO_LDFLAGS="$(GO_LDFLAGS)" -COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" -COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" -COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION="$(PROTOBUF_GO_VERSION)" COMMON_ARGS += --build-arg=GRPC_GO_VERSION="$(GRPC_GO_VERSION)" COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION="$(GRPC_GATEWAY_VERSION)" COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION="$(VTPROTOBUF_VERSION)" COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)" +COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" +COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" +COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)" TOOLCHAIN ?= docker.io/golang:1.20-alpine @@ -150,7 +150,7 @@ unit-tests-race: ## Performs unit tests with race detection enabled. .PHONY: coverage coverage: ## Upload coverage data to codecov.io. - bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/coverage.txt -X fix" + bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/coverage-unit-tests.txt -X fix" .PHONY: $(ARTIFACTS)/kres-linux-amd64 $(ARTIFACTS)/kres-linux-amd64: diff --git a/internal/output/drone/step.go b/internal/output/drone/step.go index f9264ee..5ca987b 100644 --- a/internal/output/drone/step.go +++ b/internal/output/drone/step.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/drone/drone-yaml/yaml" + "github.com/siderolabs/gen/slices" ) // Step is a pipeline Step. @@ -64,7 +65,15 @@ func (step *Step) EnvironmentFromSecret(name, secretName string) *Step { // DependsOn appends to a list of step dependencies. func (step *Step) DependsOn(depends ...string) *Step { - step.container.DependsOn = append(step.container.DependsOn, depends...) + for _, dep := range depends { + if slices.Contains(step.container.DependsOn, func(d string) bool { + return d == dep + }) { + continue + } + + step.container.DependsOn = append(step.container.DependsOn, dep) + } return step } diff --git a/internal/output/golangci/golangci.go b/internal/output/golangci/golangci.go index 3a278a5..b9f58bb 100644 --- a/internal/output/golangci/golangci.go +++ b/internal/output/golangci/golangci.go @@ -9,6 +9,9 @@ import ( _ "embed" "fmt" "io" + "path/filepath" + + "github.com/siderolabs/gen/slices" "github.com/siderolabs/kres/internal/output" ) @@ -24,15 +27,18 @@ var configTemplate string type Output struct { output.FileAdapter + files []file + enabled bool +} + +type file struct { + path string canonicalPath string - enabled bool } // NewOutput creates new Makefile output. func NewOutput() *Output { - output := &Output{ - canonicalPath: "github.com/example.com/example.proj", - } + output := &Output{} output.FileAdapter.FileWriter = output @@ -49,9 +55,12 @@ func (o *Output) Enable() { o.enabled = true } -// CanonicalPath sets canonical import path. -func (o *Output) CanonicalPath(path string) { - o.canonicalPath = path +// NewFile sets canonical import path and project path. +func (o *Output) NewFile(canonicalPath, path string) { + o.files = append(o.files, file{ + path: filepath.Join(path, filename), + canonicalPath: canonicalPath, + }) } // Filenames implements output.FileWriter interface. @@ -60,25 +69,28 @@ func (o *Output) Filenames() []string { return nil } - return []string{filename} + return slices.Map(o.files, func(f file) string { return f.path }) } // GenerateFile implements output.FileWriter interface. func (o *Output) GenerateFile(filename string, w io.Writer) error { - switch filename { - case filename: - return o.config(w) - default: - panic("unexpected filename: " + filename) + index := slices.IndexFunc(o.files, func(f file) bool { + return f.path == filename + }) + + if index >= 0 { + return o.config(o.files[index], w) } + + panic("unexpected filename: " + filename) } -func (o *Output) config(w io.Writer) error { +func (o *Output) config(f file, w io.Writer) error { if _, err := w.Write([]byte(output.Preamble("# "))); err != nil { return err } - if _, err := fmt.Fprintf(w, configTemplate, o.canonicalPath); err != nil { + if _, err := fmt.Fprintf(w, configTemplate, f.canonicalPath); err != nil { return err } diff --git a/internal/output/makefile/group.go b/internal/output/makefile/group.go index 5e7f6bc..abf2488 100644 --- a/internal/output/makefile/group.go +++ b/internal/output/makefile/group.go @@ -7,6 +7,8 @@ package makefile import ( "fmt" "io" + + "github.com/siderolabs/gen/slices" ) // Descriptions (used as keys) of some predefined variable groups. @@ -25,6 +27,12 @@ type VariableGroup struct { // Variable appends variable to the group. func (group *VariableGroup) Variable(variable *Variable) *VariableGroup { + if slices.Contains(group.variables, func(item *Variable) bool { + return item.name == variable.name + }) { + return group + } + group.variables = append(group.variables, variable) return group diff --git a/internal/output/makefile/makefile.go b/internal/output/makefile/makefile.go index 4e37d67..e628615 100644 --- a/internal/output/makefile/makefile.go +++ b/internal/output/makefile/makefile.go @@ -9,6 +9,8 @@ import ( "io" "sort" + "github.com/siderolabs/gen/slices" + "github.com/siderolabs/kres/internal/output" ) @@ -63,6 +65,13 @@ func (o *Output) Target(name string) *Target { return target } +// HasTarget checks that target exists. +func (o *Output) HasTarget(name string) bool { + return slices.Contains(o.targets, func(t *Target) bool { + return t.name == name + }) +} + // IfTrueCondition creates new Makefile condition. func (o *Output) IfTrueCondition(variable string) *Condition { condition := &Condition{ diff --git a/internal/project/auto/golang.go b/internal/project/auto/golang.go index 9a9a024..1018912 100644 --- a/internal/project/auto/golang.go +++ b/internal/project/auto/golang.go @@ -8,40 +8,91 @@ import ( "fmt" "io" "os" - "path" "path/filepath" "golang.org/x/mod/modfile" + "github.com/siderolabs/kres/internal/dag" "github.com/siderolabs/kres/internal/project/common" "github.com/siderolabs/kres/internal/project/golang" + "github.com/siderolabs/kres/internal/project/meta" "github.com/siderolabs/kres/internal/project/service" "github.com/siderolabs/kres/internal/project/wrap" ) // DetectGolang checks if project at rootPath is Go-based project. -// -//nolint:gocognit,gocyclo,cyclop func (builder *builder) DetectGolang() (bool, error) { - gomodPath := filepath.Join(builder.rootPath, "go.mod") + lookupDirs := []string{} - gomod, err := os.Open(gomodPath) - if err != nil { - if os.IsNotExist(err) { - return false, nil + err := filepath.Walk(builder.rootPath, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil } + if filepath.Base(path) == "go.mod" { + lookupDirs = append(lookupDirs, filepath.Dir(path)) + } + + return nil + }) + if err != nil { return false, err } - defer gomod.Close() //nolint:errcheck + for _, dir := range lookupDirs { + _, err := os.Stat(filepath.Join(dir, ".kresignore")) + if err == nil { + continue + } - contents, err := io.ReadAll(gomod) - if err != nil { - return true, err + if !os.IsNotExist(err) { + return false, err + } + + if err := builder.processDirectory(dir); err != nil { + return false, err + } + + builder.meta.GoRootDirectories = append(builder.meta.GoRootDirectories, dir) } - builder.meta.CanonicalPath = modfile.ModulePath(contents) + if len(builder.meta.GoSourceFiles) == 0 && len(builder.meta.GoDirectories) == 0 { + return false, fmt.Errorf("no Go source files found") + } + + return true, nil +} + +//nolint:gocognit,gocyclo,cyclop +func (builder *builder) processDirectory(path string) error { + var ( + canonicalPath string + dir = filepath.Join(builder.rootPath, path) + ) + + { + gomodPath := filepath.Join(dir, "go.mod") + + gomod, err := os.Open(gomodPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + + return err + } + + defer gomod.Close() //nolint:errcheck + + contents, err := io.ReadAll(gomod) + if err != nil { + return err + } + + canonicalPath = modfile.ModulePath(contents) + } + + builder.meta.CanonicalPaths = append(builder.meta.CanonicalPaths, canonicalPath) for _, srcDir := range []string{ "api", // API definitions (generated protobufs, Kubebuilder's resources) @@ -51,22 +102,22 @@ func (builder *builder) DetectGolang() (bool, error) { "pkg", // generic, general use packages that can be used independently "src", // deprecated } { - exists, err := directoryExists(builder.rootPath, srcDir) + exists, err := directoryExists(dir, srcDir) if err != nil { - return true, err + return err } if exists { - builder.meta.Directories = append(builder.meta.Directories, srcDir) - builder.meta.GoDirectories = append(builder.meta.GoDirectories, srcDir) + builder.meta.Directories = append(builder.meta.Directories, filepath.Join(dir, srcDir)) + builder.meta.GoDirectories = append(builder.meta.GoDirectories, filepath.Join(dir, srcDir)) } } if len(builder.meta.GoDirectories) == 0 { // no standard directories found, assume any directory with `.go` files is a source directory - topLevel, err := os.ReadDir(builder.rootPath) + topLevel, err := os.ReadDir(dir) if err != nil { - return true, err + return err } for _, item := range topLevel { @@ -74,68 +125,74 @@ func (builder *builder) DetectGolang() (bool, error) { continue } - result, err := listFilesWithSuffix(filepath.Join(builder.rootPath, item.Name()), ".go") + result, err := listFilesWithSuffix(filepath.Join(dir, item.Name()), ".go") if err != nil { - return true, err + return err } if len(result) > 0 { - builder.meta.Directories = append(builder.meta.Directories, item.Name()) - builder.meta.GoDirectories = append(builder.meta.GoDirectories, item.Name()) + builder.meta.Directories = append(builder.meta.Directories, filepath.Join(dir, item.Name())) + builder.meta.GoDirectories = append(builder.meta.GoDirectories, filepath.Join(dir, item.Name())) } } } { - list, err := listFilesWithSuffix(builder.rootPath, ".go") + list, err := listFilesWithSuffix(dir, ".go") if err != nil { - return true, err + return err } for _, item := range list { - builder.meta.SourceFiles = append(builder.meta.SourceFiles, item) - builder.meta.GoSourceFiles = append(builder.meta.GoSourceFiles, item) + builder.meta.SourceFiles = append(builder.meta.SourceFiles, filepath.Join(dir, item)) + builder.meta.GoSourceFiles = append(builder.meta.GoSourceFiles, filepath.Join(dir, item)) } } - if len(builder.meta.GoSourceFiles) == 0 && len(builder.meta.GoDirectories) == 0 { - return false, fmt.Errorf("no Go source files found") - } + builder.meta.SourceFiles = append(builder.meta.SourceFiles, + filepath.Join(dir, "go.mod"), + filepath.Join(dir, "go.sum"), + ) - builder.meta.SourceFiles = append(builder.meta.SourceFiles, "go.mod", "go.sum") + rootPath := filepath.Join(builder.rootPath, dir) - for _, candidate := range []string{"pkg/version", "internal/version"} { - exists, err := directoryExists(builder.rootPath, candidate) - if err != nil { - return true, err - } - - if exists { - builder.meta.VersionPackage = path.Join(builder.meta.CanonicalPath, candidate) - } - } - - { - cmdExists, err := directoryExists(builder.rootPath, "cmd") - if err != nil { - return true, err - } - - if cmdExists { - dirs, err := os.ReadDir(filepath.Join(builder.rootPath, "cmd")) + if builder.meta.VersionPackage == "" { + for _, candidate := range []string{"pkg/version", "internal/version"} { + exists, err := directoryExists(dir, candidate) if err != nil { - return true, err + return err } - for _, dir := range dirs { - if dir.IsDir() { - builder.meta.Commands = append(builder.meta.Commands, dir.Name()) - } + if exists { + builder.meta.VersionPackage = filepath.Join(canonicalPath, filepath.Join(dir, candidate)) } } } - return true, nil + cmdExists, err := directoryExists(rootPath, "cmd") + if err != nil { + return err + } + + if cmdExists { + path := filepath.Join(dir, "cmd") + + dirs, err := os.ReadDir(path) + if err != nil { + return err + } + + for _, dir := range dirs { + if dir.IsDir() { + builder.meta.Commands = append(builder.meta.Commands, meta.Command{ + Path: filepath.Join(path, dir.Name()), + Name: dir.Name(), + }) + } + } + } + + return nil } // BuildGolang builds project structure for Go project. @@ -144,49 +201,69 @@ func (builder *builder) BuildGolang() error { toolchain := golang.NewToolchain(builder.meta) toolchain.AddInput(builder.commonInputs...) - // linters - golangciLint := golang.NewGolangciLint(builder.meta) - gofumpt := golang.NewGofumpt(builder.meta) - govulncheck := golang.NewGoVulnCheck(builder.meta) - goimports := golang.NewGoimports(builder.meta) - - // linters are input to the toolchain as they inject into toolchain build - toolchain.AddInput(golangciLint, gofumpt, govulncheck, goimports) - // add protobufs and go generate generate := golang.NewGenerate(builder.meta) // add deepcopy deepcopy := golang.NewDeepCopy(builder.meta) - toolchain.AddInput(generate, deepcopy) + // add common linter tools + linters := golang.NewLinters(builder.meta) - builder.lintInputs = append(builder.lintInputs, toolchain, golangciLint, gofumpt, govulncheck, goimports) + toolchain.AddInput(generate, deepcopy, linters) - // unit-tests - unitTests := golang.NewUnitTests(builder.meta) - unitTests.AddInput(toolchain) + builder.lintInputs = append(builder.lintInputs, toolchain, linters) coverage := service.NewCodeCov(builder.meta) - coverage.InputPath = "coverage.txt" - coverage.AddInput(unitTests) - builder.targets = append(builder.targets, unitTests, coverage) + allUnitTests := []dag.Node{} + + // linters + for index, canonicalPath := range builder.meta.CanonicalPaths { + projectPath := builder.meta.GoRootDirectories[index] + canonicalPath += "/" + + golangciLint := golang.NewGolangciLint(builder.meta, projectPath, canonicalPath) + gofumpt := golang.NewGofumpt(builder.meta, projectPath) + govulncheck := golang.NewGoVulnCheck(builder.meta, projectPath) + goimports := golang.NewGoimports(builder.meta, projectPath, canonicalPath) + + // linters are input to the toolchain as they inject into toolchain build + toolchain.AddInput(golangciLint, gofumpt, govulncheck, goimports) + + builder.lintInputs = append(builder.lintInputs, toolchain, golangciLint, gofumpt, govulncheck, goimports) + + // unit-tests + unitTests := golang.NewUnitTests(builder.meta, projectPath) + unitTests.AddInput(toolchain) + + coverage.AddInput(unitTests) + + builder.targets = append(builder.targets, unitTests) + allUnitTests = append(allUnitTests, unitTests) + + coverage.InputPaths = append(coverage.InputPaths, fmt.Sprintf("coverage-%s.txt", unitTests.Name())) + } + + builder.targets = append(builder.targets, coverage) // process commands for _, cmd := range builder.meta.Commands { - cfg := CommandConfig{NamedConfig: NamedConfig{name: cmd}} + cfg := CommandConfig{NamedConfig: NamedConfig{name: cmd.Name}} if err := builder.meta.Config.Load(&cfg); err != nil { return err } - build := golang.NewBuild(builder.meta, cmd, filepath.Join("cmd", cmd)) + build := golang.NewBuild(builder.meta, cmd.Name, cmd.Path) build.AddInput(toolchain) builder.targets = append(builder.targets, build) if !cfg.DisableImage { - image := common.NewImage(builder.meta, cmd) - image.AddInput(build, builder.lintTarget, wrap.Drone(unitTests)) + image := common.NewImage(builder.meta, cmd.Name) + + for _, unitTests := range allUnitTests { + image.AddInput(build, builder.lintTarget, wrap.Drone(unitTests)) + } builder.targets = append(builder.targets, image) } diff --git a/internal/project/golang/gen_name.go b/internal/project/golang/gen_name.go new file mode 100644 index 0000000..fdfa2bb --- /dev/null +++ b/internal/project/golang/gen_name.go @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package golang + +import ( + "os" + "strings" +) + +func genName(name, projectPath string) string { + if projectPath == "." || projectPath == "" { + return name + } + + return strings.Join(append([]string{name}, strings.Split(projectPath, string(os.PathSeparator))...), "-") +} diff --git a/internal/project/golang/generate.go b/internal/project/golang/generate.go index eb737e5..2dd8292 100644 --- a/internal/project/golang/generate.go +++ b/internal/project/golang/generate.go @@ -265,7 +265,7 @@ func (generate *Generate) CompileDockerfile(output *dockerfile.Output) error { "goimports", "-w", "-local", - generate.meta.CanonicalPath, + strings.Join(generate.meta.CanonicalPaths, ","), generate.BaseSpecPath, ), ) @@ -300,7 +300,7 @@ func (generate *Generate) CompileDockerfile(output *dockerfile.Output) error { "goimports", "-w", "-local", - generate.meta.CanonicalPath, + strings.Join(generate.meta.CanonicalPaths, ","), spec.Source, )) } diff --git a/internal/project/golang/gofumpt.go b/internal/project/golang/gofumpt.go index 67401f3..c4f83b4 100644 --- a/internal/project/golang/gofumpt.go +++ b/internal/project/golang/gofumpt.go @@ -21,63 +21,60 @@ type Gofumpt struct { meta *meta.Options - GoVersion string `yaml:"goVersion"` - Version string `yaml:"version"` + GoVersion string `yaml:"goVersion"` + Version string `yaml:"version"` + projectPath string } // NewGofumpt builds Gofumpt node. -func NewGofumpt(meta *meta.Options) *Gofumpt { - meta.BuildArgs = append(meta.BuildArgs, "GOFUMPT_VERSION") +func NewGofumpt(meta *meta.Options, projectPath string) *Gofumpt { + meta.BuildArgs.Add("GOFUMPT_VERSION") return &Gofumpt{ - BaseNode: dag.NewBaseNode("lint-gofumpt"), + BaseNode: dag.NewBaseNode(genName("lint-gofumpt", projectPath)), meta: meta, - GoVersion: config.GoVersion, - Version: config.GoFmtVersion, + GoVersion: config.GoVersion, + Version: config.GoFmtVersion, + projectPath: projectPath, } } // CompileMakefile implements makefile.Compiler. func (lint *Gofumpt) CompileMakefile(output *makefile.Output) error { - output.Target("lint-gofumpt").Description("Runs gofumpt linter."). + output.Target(lint.Name()).Description("Runs gofumpt linter."). Script("@$(MAKE) target-$@") output.VariableGroup(makefile.VariableGroupCommon). Variable(makefile.OverridableVariable("GOFUMPT_VERSION", lint.Version)). Variable(makefile.OverridableVariable("GO_VERSION", lint.GoVersion)) - output.Target("fmt").Description("Formats the source code"). - Phony(). - Script( - `@docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \ + if !output.HasTarget("fmt") { + output.Target("fmt").Description("Formats the source code"). + Phony(). + Script( + `@docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \ bash -c "export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \ gofumpt -w ."`, - ) - - return nil -} - -// ToolchainBuild implements common.ToolchainBuilder hook. -func (lint *Gofumpt) ToolchainBuild(stage *dockerfile.Stage) error { - stage. - Step(step.Arg("GOFUMPT_VERSION")). - Step(step.Script(fmt.Sprintf( - `go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ - && mv /go/bin/gofumpt %s/gofumpt`, lint.meta.BinPath))) + ) + } return nil } // CompileDockerfile implements dockerfile.Compiler. func (lint *Gofumpt) CompileDockerfile(output *dockerfile.Output) error { - output.Stage("lint-gofumpt"). + output.Stage(lint.Name()). Description("runs gofumpt"). From("base"). Step(step.Script( - `FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w .':\n${FILES}"; exit 1)`, + fmt.Sprintf( + `FILES="$(gofumpt -l %s)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w %s':\n${FILES}"; exit 1)`, + lint.projectPath, + lint.projectPath, + ), )) return nil diff --git a/internal/project/golang/gofumpt_test.go b/internal/project/golang/gofumpt_test.go index 39c8de3..84ba15e 100644 --- a/internal/project/golang/gofumpt_test.go +++ b/internal/project/golang/gofumpt_test.go @@ -11,12 +11,10 @@ import ( "github.com/siderolabs/kres/internal/output/dockerfile" "github.com/siderolabs/kres/internal/output/makefile" - "github.com/siderolabs/kres/internal/project/common" "github.com/siderolabs/kres/internal/project/golang" ) func TestGofumptInterfaces(t *testing.T) { assert.Implements(t, (*dockerfile.Compiler)(nil), new(golang.Gofumpt)) assert.Implements(t, (*makefile.Compiler)(nil), new(golang.Gofumpt)) - assert.Implements(t, (*common.ToolchainBuilder)(nil), new(golang.Gofumpt)) } diff --git a/internal/project/golang/goimports.go b/internal/project/golang/goimports.go index ab3f73d..9cc4ae0 100644 --- a/internal/project/golang/goimports.go +++ b/internal/project/golang/goimports.go @@ -6,7 +6,6 @@ package golang import ( "fmt" - "path/filepath" "github.com/siderolabs/kres/internal/config" "github.com/siderolabs/kres/internal/dag" @@ -22,25 +21,27 @@ type Goimports struct { meta *meta.Options - Version string `yaml:"version"` + Version string `yaml:"version"` + canonicalPath string + projectPath string } // NewGoimports builds Goimports node. -func NewGoimports(meta *meta.Options) *Goimports { - meta.BuildArgs = append(meta.BuildArgs, "GOIMPORTS_VERSION") - +func NewGoimports(meta *meta.Options, projectPath, canonicalPath string) *Goimports { return &Goimports{ - BaseNode: dag.NewBaseNode("lint-goimports"), + BaseNode: dag.NewBaseNode(genName("lint-goimports", projectPath)), meta: meta, - Version: config.GoImportsVersion, + Version: config.GoImportsVersion, + canonicalPath: canonicalPath, + projectPath: projectPath, } } // CompileMakefile implements makefile.Compiler. func (lint *Goimports) CompileMakefile(output *makefile.Output) error { - output.Target("lint-goimports").Description("Runs goimports linter."). + output.Target(lint.Name()).Description("Runs goimports linter."). Script("@$(MAKE) target-$@") output.VariableGroup(makefile.VariableGroupCommon). @@ -49,30 +50,18 @@ func (lint *Goimports) CompileMakefile(output *makefile.Output) error { return nil } -// ToolchainBuild implements common.ToolchainBuilder hook. -func (lint *Goimports) ToolchainBuild(stage *dockerfile.Stage) error { - stage. - Step(step.Arg("GOIMPORTS_VERSION")). - Step(step.Script(fmt.Sprintf( - `go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} \ - && mv /go/bin/goimports %s/goimports`, lint.meta.BinPath)). - MountCache(filepath.Join(lint.meta.CachePath, "go-build")). - MountCache(filepath.Join(lint.meta.GoPath, "pkg")), - ) - - return nil -} - // CompileDockerfile implements dockerfile.Compiler. func (lint *Goimports) CompileDockerfile(output *dockerfile.Output) error { - output.Stage("lint-goimports"). + output.Stage(lint.Name()). Description("runs goimports"). From("base"). Step(step.Script( fmt.Sprintf( - `FILES="$(goimports -l -local %s .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local %s .':\n${FILES}"; exit 1)`, - lint.meta.CanonicalPath, - lint.meta.CanonicalPath, + `FILES="$(goimports -l -local %s %s)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'goimports -w -local %s %s':\n${FILES}"; exit 1)`, + lint.canonicalPath, + lint.projectPath, + lint.canonicalPath, + lint.projectPath, ), )) diff --git a/internal/project/golang/goimports_test.go b/internal/project/golang/goimports_test.go index 707e973..5e94c5d 100644 --- a/internal/project/golang/goimports_test.go +++ b/internal/project/golang/goimports_test.go @@ -11,12 +11,10 @@ import ( "github.com/siderolabs/kres/internal/output/dockerfile" "github.com/siderolabs/kres/internal/output/makefile" - "github.com/siderolabs/kres/internal/project/common" "github.com/siderolabs/kres/internal/project/golang" ) func TestGoimportsInterfaces(t *testing.T) { assert.Implements(t, (*dockerfile.Compiler)(nil), new(golang.Goimports)) assert.Implements(t, (*makefile.Compiler)(nil), new(golang.Goimports)) - assert.Implements(t, (*common.ToolchainBuilder)(nil), new(golang.Goimports)) } diff --git a/internal/project/golang/golangcilint.go b/internal/project/golang/golangcilint.go index c080979..80a7fd2 100644 --- a/internal/project/golang/golangcilint.go +++ b/internal/project/golang/golangcilint.go @@ -5,7 +5,6 @@ package golang import ( - "fmt" "path/filepath" "github.com/siderolabs/kres/internal/config" @@ -23,34 +22,37 @@ type GolangciLint struct { meta *meta.Options - Version string + Version string + canonicalPath string + projectPath string } // NewGolangciLint builds golangci-lint node. -func NewGolangciLint(meta *meta.Options) *GolangciLint { - meta.SourceFiles = append(meta.SourceFiles, ".golangci.yml") - meta.BuildArgs = append(meta.BuildArgs, "GOLANGCILINT_VERSION") +func NewGolangciLint(meta *meta.Options, projectPath, canonicalPath string) *GolangciLint { + meta.SourceFiles = append(meta.SourceFiles, filepath.Join(projectPath, ".golangci.yml")) return &GolangciLint{ - BaseNode: dag.NewBaseNode("lint-golangci-lint"), + BaseNode: dag.NewBaseNode(genName("lint-golangci-lint", projectPath)), meta: meta, - Version: config.GolangCIlintVersion, + Version: config.GolangCIlintVersion, + canonicalPath: canonicalPath, + projectPath: projectPath, } } // CompileGolangci implements golangci.Compiler. func (lint *GolangciLint) CompileGolangci(output *golangci.Output) error { output.Enable() - output.CanonicalPath(lint.meta.CanonicalPath) + output.NewFile(lint.canonicalPath, lint.projectPath) return nil } // CompileMakefile implements makefile.Compiler. func (lint *GolangciLint) CompileMakefile(output *makefile.Output) error { - output.Target("lint-golangci-lint").Description("Runs golangci-lint linter."). + output.Target(lint.Name()).Description("Runs golangci-lint linter."). Script("@$(MAKE) target-$@") output.VariableGroup(makefile.VariableGroupCommon). @@ -59,28 +61,13 @@ func (lint *GolangciLint) CompileMakefile(output *makefile.Output) error { return nil } -// ToolchainBuild implements common.ToolchainBuilder hook. -func (lint *GolangciLint) ToolchainBuild(stage *dockerfile.Stage) error { - stage. - Step(step.Arg("GOLANGCILINT_VERSION")). - Step(step.Script( - fmt.Sprintf( - "go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \\\n"+ - "\t&& mv /go/bin/golangci-lint %s/golangci-lint", lint.meta.BinPath), - ). - MountCache(filepath.Join(lint.meta.CachePath, "go-build")). - MountCache(filepath.Join(lint.meta.GoPath, "pkg")), - ) - - return nil -} - // CompileDockerfile implements dockerfile.Compiler. func (lint *GolangciLint) CompileDockerfile(output *dockerfile.Output) error { - output.Stage("lint-golangci-lint"). + output.Stage(lint.Name()). Description("runs golangci-lint"). From("base"). - Step(step.Copy(".golangci.yml", ".")). + Step(step.WorkDir(filepath.Join("/src", lint.projectPath))). + Step(step.Copy(filepath.Join(lint.projectPath, ".golangci.yml"), ".")). Step(step.Env("GOGC", "50")). Step(step.Run("golangci-lint", "run", "--config", ".golangci.yml"). MountCache(filepath.Join(lint.meta.CachePath, "go-build")). diff --git a/internal/project/golang/golangcilint_test.go b/internal/project/golang/golangcilint_test.go index dd5c152..5878cf7 100644 --- a/internal/project/golang/golangcilint_test.go +++ b/internal/project/golang/golangcilint_test.go @@ -11,12 +11,10 @@ import ( "github.com/siderolabs/kres/internal/output/dockerfile" "github.com/siderolabs/kres/internal/output/makefile" - "github.com/siderolabs/kres/internal/project/common" "github.com/siderolabs/kres/internal/project/golang" ) func TestGolangciLintInterfaces(t *testing.T) { assert.Implements(t, (*dockerfile.Compiler)(nil), new(golang.GolangciLint)) assert.Implements(t, (*makefile.Compiler)(nil), new(golang.GolangciLint)) - assert.Implements(t, (*common.ToolchainBuilder)(nil), new(golang.GolangciLint)) } diff --git a/internal/project/golang/govulncheck.go b/internal/project/golang/govulncheck.go index 005b88c..57de50e 100644 --- a/internal/project/golang/govulncheck.go +++ b/internal/project/golang/govulncheck.go @@ -5,7 +5,6 @@ package golang import ( - "fmt" "path/filepath" "github.com/siderolabs/kres/internal/dag" @@ -16,49 +15,37 @@ import ( ) // GoVulnCheck provides GoVulnCheck linter. -// -//nolint:govet type GoVulnCheck struct { dag.BaseNode - meta *meta.Options + meta *meta.Options + projectPath string } // NewGoVulnCheck builds GoVulnCheck node. -func NewGoVulnCheck(meta *meta.Options) *GoVulnCheck { +func NewGoVulnCheck(meta *meta.Options, projectPath string) *GoVulnCheck { return &GoVulnCheck{ - BaseNode: dag.NewBaseNode("lint-govulncheck"), + BaseNode: dag.NewBaseNode(genName("lint-govulncheck", projectPath)), - meta: meta, + meta: meta, + projectPath: projectPath, } } // CompileMakefile implements makefile.Compiler. func (lint *GoVulnCheck) CompileMakefile(output *makefile.Output) error { - output.Target("lint-govulncheck").Description("Runs govulncheck linter."). + output.Target(lint.Name()).Description("Runs govulncheck linter."). Script("@$(MAKE) target-$@") return nil } -// ToolchainBuild implements common.ToolchainBuilder hook. -func (lint *GoVulnCheck) ToolchainBuild(stage *dockerfile.Stage) error { - stage. - Step(step.Script(fmt.Sprintf( - `go install golang.org/x/vuln/cmd/govulncheck@latest \ - && mv /go/bin/govulncheck %s/govulncheck`, lint.meta.BinPath)). - MountCache(filepath.Join(lint.meta.CachePath, "go-build")). - MountCache(filepath.Join(lint.meta.GoPath, "pkg")), - ) - - return nil -} - // CompileDockerfile implements dockerfile.Compiler. func (lint *GoVulnCheck) CompileDockerfile(output *dockerfile.Output) error { - output.Stage("lint-govulncheck"). + output.Stage(lint.Name()). Description("runs govulncheck"). From("base"). + Step(step.WorkDir(filepath.Join("/src", lint.projectPath))). Step(step.Script( `govulncheck ./...`, ). diff --git a/internal/project/golang/govulncheck_test.go b/internal/project/golang/govulncheck_test.go index cfd340e..4a445ea 100644 --- a/internal/project/golang/govulncheck_test.go +++ b/internal/project/golang/govulncheck_test.go @@ -11,12 +11,10 @@ import ( "github.com/siderolabs/kres/internal/output/dockerfile" "github.com/siderolabs/kres/internal/output/makefile" - "github.com/siderolabs/kres/internal/project/common" "github.com/siderolabs/kres/internal/project/golang" ) func TestGoVulnCheckInterfaces(t *testing.T) { assert.Implements(t, (*dockerfile.Compiler)(nil), new(golang.GoVulnCheck)) assert.Implements(t, (*makefile.Compiler)(nil), new(golang.GoVulnCheck)) - assert.Implements(t, (*common.ToolchainBuilder)(nil), new(golang.GoVulnCheck)) } diff --git a/internal/project/golang/linters.go b/internal/project/golang/linters.go new file mode 100644 index 0000000..4a87f9e --- /dev/null +++ b/internal/project/golang/linters.go @@ -0,0 +1,74 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package golang + +import ( + "fmt" + "path/filepath" + + "github.com/siderolabs/kres/internal/dag" + "github.com/siderolabs/kres/internal/output/dockerfile" + "github.com/siderolabs/kres/internal/output/dockerfile/step" + "github.com/siderolabs/kres/internal/project/meta" +) + +// Linters is the common node for all linters. +type Linters struct { + meta *meta.Options + + dag.BaseNode +} + +// NewLinters builds GoVulnCheck node. +func NewLinters(meta *meta.Options) *Linters { + meta.BuildArgs.Add( + "GOLANGCILINT_VERSION", + "GOIMPORTS_VERSION", + "GOFUMPT_VERSION", + ) + + return &Linters{ + BaseNode: dag.NewBaseNode("go-linters"), + + meta: meta, + } +} + +// ToolchainBuild implements common.ToolchainBuilder hook. +func (linters *Linters) ToolchainBuild(stage *dockerfile.Stage) error { + stage. + Step(step.Arg("GOLANGCILINT_VERSION")). + Step(step.Script( + fmt.Sprintf( + "go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \\\n"+ + "\t&& mv /go/bin/golangci-lint %s/golangci-lint", linters.meta.BinPath), + ). + MountCache(filepath.Join(linters.meta.CachePath, "go-build")). + MountCache(filepath.Join(linters.meta.GoPath, "pkg")), + ). + Step(step.Script(fmt.Sprintf( + `go install golang.org/x/vuln/cmd/govulncheck@latest \ + && mv /go/bin/govulncheck %s/govulncheck`, linters.meta.BinPath)). + MountCache(filepath.Join(linters.meta.CachePath, "go-build")). + MountCache(filepath.Join(linters.meta.GoPath, "pkg")), + ). + Step(step.Arg("GOIMPORTS_VERSION")). + Step(step.Script(fmt.Sprintf( + `go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} \ + && mv /go/bin/goimports %s/goimports`, linters.meta.BinPath)). + MountCache(filepath.Join(linters.meta.CachePath, "go-build")). + MountCache(filepath.Join(linters.meta.GoPath, "pkg")), + ). + Step(step.Arg("GOFUMPT_VERSION")). + Step(step.Script(fmt.Sprintf( + `go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ + && mv /go/bin/gofumpt %s/gofumpt`, linters.meta.BinPath))) + + return nil +} + +// SkipAsMakefileDependency implements makefile.SkipAsMakefileDependency. +func (linters *Linters) SkipAsMakefileDependency() { +} diff --git a/internal/project/golang/linters_test.go b/internal/project/golang/linters_test.go new file mode 100644 index 0000000..6108a38 --- /dev/null +++ b/internal/project/golang/linters_test.go @@ -0,0 +1,18 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package golang_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/kres/internal/project/common" + "github.com/siderolabs/kres/internal/project/golang" +) + +func TestLintersInterfaces(t *testing.T) { + assert.Implements(t, (*common.ToolchainBuilder)(nil), new(golang.Linters)) +} diff --git a/internal/project/golang/toolchain.go b/internal/project/golang/toolchain.go index a99b91b..467c2d1 100644 --- a/internal/project/golang/toolchain.go +++ b/internal/project/golang/toolchain.go @@ -208,11 +208,21 @@ func (toolchain *Toolchain) CompileDockerfile(output *dockerfile.Output) error { base := output.Stage("base"). Description("tools and sources"). From("tools"). - Step(step.WorkDir("/src")). - Step(step.Copy("./go.mod", ".")). - Step(step.Copy("./go.sum", ".")). - Step(step.Run("go", "mod", "download").MountCache(filepath.Join(toolchain.meta.GoPath, "pkg"))). - Step(step.Run("go", "mod", "verify").MountCache(filepath.Join(toolchain.meta.GoPath, "pkg"))) + Step(step.WorkDir("/src")) + + for _, rootDir := range toolchain.meta.GoRootDirectories { + gomodPath := filepath.Join(rootDir, "go.mod") + gosumPath := filepath.Join(rootDir, "go.sum") + + base.Step(step.Copy(gomodPath, gomodPath)). + Step(step.Copy(gosumPath, gosumPath)) + } + + for _, rootDir := range toolchain.meta.GoRootDirectories { + base.Step(step.Run("cd", rootDir)). + Step(step.Run("go", "mod", "download").MountCache(filepath.Join(toolchain.meta.GoPath, "pkg"))). + Step(step.Run("go", "mod", "verify").MountCache(filepath.Join(toolchain.meta.GoPath, "pkg"))) + } for _, directory := range toolchain.meta.GoDirectories { base.Step(step.Copy("./"+directory, "./"+directory)) diff --git a/internal/project/golang/unit_tests.go b/internal/project/golang/unit_tests.go index b3e28a2..1b2837c 100644 --- a/internal/project/golang/unit_tests.go +++ b/internal/project/golang/unit_tests.go @@ -22,18 +22,20 @@ type UnitTests struct { //nolint:govet RequiresInsecure bool `yaml:"requiresInsecure"` // ExtraArgs are extra arguments for `go test`. - ExtraArgs string `yaml:"extraArgs"` + ExtraArgs string `yaml:"extraArgs"` + packagePath string meta *meta.Options } // NewUnitTests initializes UnitTests. -func NewUnitTests(meta *meta.Options) *UnitTests { - meta.BuildArgs = append(meta.BuildArgs, "TESTPKGS") +func NewUnitTests(meta *meta.Options, packagePath string) *UnitTests { + meta.BuildArgs.Add("TESTPKGS") return &UnitTests{ - BaseNode: dag.NewBaseNode("unit-tests"), - meta: meta, + BaseNode: dag.NewBaseNode(genName("unit-tests", packagePath)), + meta: meta, + packagePath: packagePath, } } @@ -52,9 +54,13 @@ func (tests *UnitTests) CompileDockerfile(output *dockerfile.Output) error { extraArgs += " " } - output.Stage("unit-tests-run"). + workdir := step.WorkDir(filepath.Join("/src", tests.packagePath)) + testRun := fmt.Sprintf("%s-run", tests.Name()) + + output.Stage(testRun). Description("runs unit-tests"). From("base"). + Step(workdir). Step(step.Arg("TESTPKGS")). Step(wrapAsInsecure( step.Script( @@ -66,13 +72,14 @@ func (tests *UnitTests) CompileDockerfile(output *dockerfile.Output) error { MountCache(filepath.Join(tests.meta.GoPath, "pkg")). MountCache("/tmp"))) - output.Stage("unit-tests"). + output.Stage(tests.Name()). From("scratch"). - Step(step.Copy("/src/coverage.txt", "/coverage.txt").From("unit-tests-run")) + Step(step.Copy(filepath.Join("/src", tests.packagePath, "coverage.txt"), fmt.Sprintf("/coverage-%s.txt", tests.Name())).From(testRun)) - output.Stage("unit-tests-race"). + output.Stage(fmt.Sprintf("%s-race", tests.Name())). Description("runs unit-tests with race detector"). From("base"). + Step(workdir). Step(step.Arg("TESTPKGS")). Step(wrapAsInsecure( step.Script( @@ -100,12 +107,12 @@ func (tests *UnitTests) CompileMakefile(output *makefile.Output) error { scriptExtraArgs += ` TARGET_ARGS="--allow security.insecure"` } - output.Target("unit-tests"). + output.Target(tests.Name()). Description("Performs unit tests"). Script(fmt.Sprintf("@$(MAKE) local-$@ DEST=$(ARTIFACTS)%s", scriptExtraArgs)). Phony() - output.Target("unit-tests-race"). + output.Target(fmt.Sprintf("%s-race", tests.Name())). Description("Performs unit tests with race detection enabled."). Script(fmt.Sprintf("@$(MAKE) target-$@%s", scriptExtraArgs)). Phony() @@ -115,11 +122,11 @@ func (tests *UnitTests) CompileMakefile(output *makefile.Output) error { // CompileDrone implements drone.Compiler. func (tests *UnitTests) CompileDrone(output *drone.Output) error { - output.Step(drone.MakeStep("unit-tests"). + output.Step(drone.MakeStep(tests.Name()). DependsOn(dag.GatherMatchingInputNames(tests, dag.Implements[drone.Compiler]())...), ) - output.Step(drone.MakeStep("unit-tests-race"). + output.Step(drone.MakeStep(fmt.Sprintf("%s-race", tests.Name())). DependsOn(dag.GatherMatchingInputNames(tests, dag.Implements[drone.Compiler]())...), ) diff --git a/internal/project/meta/meta.go b/internal/project/meta/meta.go index 1a424d3..9d65758 100644 --- a/internal/project/meta/meta.go +++ b/internal/project/meta/meta.go @@ -5,7 +5,11 @@ // Package meta provides project options from source code. package meta -import "github.com/siderolabs/kres/internal/config" +import ( + "github.com/siderolabs/gen/slices" + + "github.com/siderolabs/kres/internal/config" +) // Options for the project. type Options struct { //nolint:govet @@ -19,8 +23,8 @@ type Options struct { //nolint:govet // Git settings. MainBranch string - // CanonicalPath, import path for Go projects. - CanonicalPath string + // CanonicalPaths, import path for Go projects. + CanonicalPaths []string // VersionPackage is a canonical path to version package (if any). VersionPackage string @@ -53,10 +57,13 @@ type Options struct { //nolint:govet MarkdownSourceFiles []string // Commands are top-level binaries to be built. - Commands []string + Commands []Command + + // GoRootDirectories contans the list of all go.mod root directories. + GoRootDirectories []string // BuildArgs passed down to Dockerfiles. - BuildArgs []string + BuildArgs BuildArgs // Path to /bin. BinPath string @@ -76,3 +83,28 @@ type Options struct { //nolint:govet // ArtifactsPath binary output path. ArtifactsPath string } + +// Command defines Golang executable build configuration. +type Command struct { + // Path defines command source path. + Path string + + // Name defines command name. + Name string +} + +// BuildArgs defines input argument list. +type BuildArgs []string + +// Add adds the args to list if it doesn't exist already. +func (args *BuildArgs) Add(arg ...string) { + for _, value := range arg { + if slices.Contains(*args, func(a string) bool { + return a == value + }) { + continue + } + + *args = append(*args, value) + } +} diff --git a/internal/project/service/codecov.go b/internal/project/service/codecov.go index 9c7d916..62f087d 100644 --- a/internal/project/service/codecov.go +++ b/internal/project/service/codecov.go @@ -20,9 +20,9 @@ type CodeCov struct { meta *meta.Options - InputPath string `yaml:"inputPath"` - TargetThreshold int `yaml:"targetThreshold"` - Enabled bool `yaml:"enabled"` + InputPaths []string `yaml:"inputPaths"` + TargetThreshold int `yaml:"targetThreshold"` + Enabled bool `yaml:"enabled"` } // NewCodeCov initializes CodeCov. @@ -57,9 +57,12 @@ func (coverage *CodeCov) CompileMakefile(output *makefile.Output) error { return nil } - output.Target("coverage").Description("Upload coverage data to codecov.io."). - Script(fmt.Sprintf(`bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/%s -X fix"`, coverage.InputPath)). - Phony() + target := output.Target("coverage").Description("Upload coverage data to codecov.io.") + + for _, inputPath := range coverage.InputPaths { + target.Script(fmt.Sprintf(`bash -c "bash <(curl -s https://codecov.io/bash) -f $(ARTIFACTS)/%s -X fix"`, inputPath)). + Phony() + } return nil }