Skip to content

Commit

Permalink
Replace deprecated gobin with custom go install based task runner
Browse files Browse the repository at this point in the history
GH-89 [1] supersedes GH-78 [2] which documents how the official
deprecation [3] of `gobin` [4] in favor of the new Go 1.16
`go install pkg@version` [5] syntax feature should have been handled for
this project. The idea was to replace the `gobin` task runner [6] with a
one that leverages "bingo" [7], a project similar to `gobin`, that comes
with many great features and also allows to manage development tools on
a per-module basis. The problem is that `bingo` uses some non-default
and nontransparent mechanisms under the hood and automatically generates
files in the repository without the option to disable this behavior.
It does not make use of the `go install` command but relies on custom
dependency resolution mechanisms, making it prone to future changes in
the Go toolchain and therefore not a good choice for the maintainability
of projects.

>>> `go install` is still not perfect

Support for the new `go install` features, which allow to install
commands without affecting the `main` module, have already been added in
GH-71 [8] as an alternative to `gobin`, but one significant problem was
still not addressed: install module/package executables globally without
overriding already installed executables of different versions.
Since `go install` will always place compiled binaries in the path
defined by `go env GOBIN`, any already existing executable with the same
name will be replaced. It is not possible to install a module command
with two different versions since `go install` still messes up the local
user environment.

>>> The Workaround: Hybrid `go install` task runner

This commit therefore implements the solution through a custom
`Runner` [9] that uses `go install` under the hood, but places the
compiled executable in a custom cache directory instead of
`go env GOBIN`. The runner checks if the executable already exists,
installs it if not so, and executes it afterwards.

The concept of storing dependencies locally on a per-project basis is
well-known from the `node_modules` directory [10] of the "Node" [11]
package manager "npm" [12]. Storing executables in a cache directory
within the repository (not tracked by Git) allows to use `go install`
mechanisms while not affect the global user environment and executables
stored in `go env GOBIN`. The runner achieves this by changing the
`GOBIN` environment variable to the custom cache directory during the
execution of `go install`. This way it bypasses the need for
"dirty hacks" while using a custom output path.

The only known disadvantage is the increased usage of storage disk
space, but since most Go executables are small in size anyway, this is
perfectly acceptable compared to the clearly outweighing advantages.

Note that the runner dynamically runs executables based on the given
task so `Validate() error` is a NOOP.

>>> Upcoming Changes

The solution described above works totally fine, but is still not a
clean solution that uses the Go toolchain without any special logic so
as soon as the following changes are made to the
Go toolchain (Go 1.17 or later), the custom runner will be removed
again:

- golang/go/issues#42088 [13] — tracks the process of adding support for
  the Go module syntax to the `go run` command. This will allow to let
  the Go toolchain handle the way how compiled executable are stored,
  located and executed.
- golang/go#44469 [14] — tracks the process of making `go install`
  aware of the `-o` flag like the `go build` command which is the only
  reason why the custom runner has been implemented.

>>> Further Adjustments

Because the new custom task runner dynamically runs executables based on
the given task, the `Bootstrap` method [15] of the `Wand` [16] reference
implementation `Elder` [17] additionally allows to pass Go module import
paths, optionally including a version suffix (`pkg@version`), to install
executables from Go module-based `main` packages into the local cache
directory. This way the local development environment can be set up,
for e.g. by running it as startup task [18] in "JetBrains" IDEs.
The method also ensures that the local cache directory exists and
creates a `.gitignore` file that includes ignore pattern for the cache
directory.

[1]: #89
[2]: #78
[3]: myitcv/gobin#103
[4]: https://github.com/myitcv/gobin
[5]: https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies
[6]: https://pkg.go.dev/github.com/svengreb/[email protected]/pkg/task/gobin#Runner
[7]: https://github.com/bwplotka/bingo
[8]: #71
[9]: https://pkg.go.dev/github.com/svengreb/wand/pkg/task#Runner
[10]: https://docs.npmjs.com/cli/v7/configuring-npm/folders#node-modules
[11]: https://nodejs.org
[12]: https://www.npmjs.com
[13]: golang/go#42088
[14]: golang/go#44469 (comment)
[15]: https://pkg.go.dev/github.com/svengreb/[email protected]/pkg/elder#Elder.Bootstrap
[16]: https://pkg.go.dev/github.com/svengreb/wand#Wand
[17]: https://pkg.go.dev/github.com/svengreb/wand/pkg/elder#Elder
[18]: https://www.jetbrains.com/help/idea/settings-tools-startup-tasks.html

GH-89
  • Loading branch information
svengreb committed Apr 25, 2021
1 parent 8b30110 commit 7e77e44
Show file tree
Hide file tree
Showing 16 changed files with 675 additions and 112 deletions.
73 changes: 33 additions & 40 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions examples/custom_runner/runners.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (r *FruitMixerRunner) Handles() task.Kind {
// Run runs the command.
// It returns an error of type *task.ErrRunner when any error occurs during the command execution.
func (r *FruitMixerRunner) Run(t task.Task) error {
tExec, tErr := r.runPrepare(t)
tExec, tErr := r.prepareTask(t)
if tErr != nil {
return tErr
}
Expand All @@ -67,7 +67,7 @@ func (r *FruitMixerRunner) Run(t task.Task) error {
// RunOut runs the command and returns its output.
// It returns an error of type *task.ErrRunner when any error occurs during the command execution.
func (r *FruitMixerRunner) RunOut(t task.Task) (string, error) {
tExec, tErr := r.runPrepare(t)
tExec, tErr := r.prepareTask(t)
if tErr != nil {
return "", tErr
}
Expand Down Expand Up @@ -102,8 +102,8 @@ func (r *FruitMixerRunner) Validate() error {
return nil
}

// runPrepare checks if the given task is of type task.Exec and prepares the task specific environment.
func (r *FruitMixerRunner) runPrepare(t task.Task) (task.Exec, error) {
// prepareTask checks if the given task is of type task.Exec and prepares the task specific environment.
func (r *FruitMixerRunner) prepareTask(t task.Task) (task.Exec, error) {
tExec, ok := t.(task.Exec)
if t.Kind() != task.KindExec || !ok {
return nil, &task.ErrRunner{
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module github.com/svengreb/wand

go 1.15
go 1.16

require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/imdario/mergo v0.3.12
github.com/magefile/mage v1.11.0
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.7.0
github.com/svengreb/golib v0.1.0
github.com/svengreb/nib v0.2.0
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
Expand All @@ -16,7 +15,6 @@ github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
Expand All @@ -30,7 +28,6 @@ github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2Jg
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
Expand All @@ -54,15 +51,15 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down Expand Up @@ -97,7 +94,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
48 changes: 48 additions & 0 deletions internal/support/exec/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2019-present Sven Greb <[email protected]>
// This source code is licensed under the MIT license found in the LICENSE file.

package exec

import (
"fmt"
"os"
"os/exec"
"path/filepath"

glFS "github.com/svengreb/golib/pkg/io/fs"

taskGo "github.com/svengreb/wand/pkg/task/golang"
)

// GetGoExecPath looks up the executable search path(s) of the current environment for the Go executable with the given
// name. It looks up the paths defined in the system "PATH" environment variable and continues with the Go specific
// "GOBIN" path.
// See https://pkg.go.dev/cmd/go#hdr-Environment_variables for more details about Go specific environment variables.
func GetGoExecPath(name string) (string, error) {
// Look up the system executable search path(s)...
execPath, pathErr := exec.LookPath(name)
os.Environ()

// ...and continue with the Go specific executable search path.
if pathErr != nil {
var execDir string

if execDir = os.Getenv(taskGo.DefaultEnvVarGOBIN); execDir == "" {
if execDir = os.Getenv(taskGo.DefaultEnvVarGOPATH); execDir != "" {
execDir = filepath.Join(execDir, taskGo.DefaultGOBINSubDirName)
}
}

execPath = filepath.Join(execDir, name)
execExits, fsErr := glFS.RegularFileExists(execPath)
if fsErr != nil {
return "", fmt.Errorf("check if %q exists: %w", execPath, fsErr)
}

if !execExits {
return "", fmt.Errorf("%q not found in executable search path(s): %v", name, append(os.Environ(), execDir))
}
}

return execPath, nil
}
92 changes: 57 additions & 35 deletions pkg/elder/elder.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,52 @@ import (
"github.com/svengreb/wand/pkg/project"
"github.com/svengreb/wand/pkg/task"
taskFSClean "github.com/svengreb/wand/pkg/task/fs/clean"
taskGobin "github.com/svengreb/wand/pkg/task/gobin"
taskGofumpt "github.com/svengreb/wand/pkg/task/gofumpt"
taskGoimports "github.com/svengreb/wand/pkg/task/goimports"
taskGo "github.com/svengreb/wand/pkg/task/golang"
taskGoBuild "github.com/svengreb/wand/pkg/task/golang/build"
taskGoTest "github.com/svengreb/wand/pkg/task/golang/test"
taskGolangCILint "github.com/svengreb/wand/pkg/task/golangcilint"
taskGoTool "github.com/svengreb/wand/pkg/task/gotool"
taskGox "github.com/svengreb/wand/pkg/task/gox"
)

// Elder is a wand.Wand reference implementation that provides common Mage tasks and stores configurations and metadata
// for applications of a project.
type Elder struct {
nib.Nib

as app.Store
gobinRunner *taskGobin.Runner
goRunner *taskGo.Runner
opts *Options
project *project.Metadata
as app.Store
goRunner *taskGo.Runner
goToolRunner *taskGoTool.Runner
opts *Options
project *project.Metadata
}

// Bootstrap runs initialization tasks to ensure the wand is operational.
// If an error occurs it will be of type *task.ErrRunner.
func (e *Elder) Bootstrap() error {
if valErr := e.gobinRunner.Validate(); valErr != nil {
e.Infof("Installing %q", e.gobinRunner.GoMod())
if installErr := e.gobinRunner.Install(e.goRunner); installErr != nil {
e.Errorf("Failed to install %q: %v", e.gobinRunner.GoMod(), installErr)
return fmt.Errorf("install %q: %w", e.gobinRunner.GoMod(), installErr)
// Bootstrap runs initialization tasks to ensure the wand is operational and sets up the local development environment
// by allowing to install executables from Go module-based "main" packages.
// The paths must be valid Go module import paths, that can optionally include the version suffix, in the "pkg@version"
// format. See https://pkg.go.dev/github.com/svengreb/wand/pkg/task/gotool for more details about the installation
// runner.
// It returns a slice of errors with type *task.ErrRunner containing any error that occurs during the execution.
func (e *Elder) Bootstrap(goModuleImportPaths ...string) []error {
var errs []error
for _, r := range []task.Runner{e.goRunner, e.goToolRunner} {
if err := r.Validate(); err != nil {
errs = append(errs, err)
}
}

return nil
for _, path := range goModuleImportPaths {
gm, gmErr := project.GoModuleFromImportPath(path)
if gmErr != nil {
errs = append(errs, gmErr)
}
if installErr := e.goToolRunner.Install(gm); installErr != nil {
errs = append(errs, installErr)
}
}

return errs
}

// Clean is a task to remove filesystem paths, e.g. output data like artifacts and reports from previous development,
Expand All @@ -66,11 +78,8 @@ func (e *Elder) Clean(appName string, opts ...taskFSClean.Option) ([]string, err
if acErr != nil {
return []string{}, fmt.Errorf("get %q application configuration: %w", appName, acErr)
}
t, tErr := taskFSClean.New(e.GetProjectMetadata(), ac, opts...)
if tErr != nil {
return []string{}, tErr
}

t := taskFSClean.New(e.GetProjectMetadata(), ac, opts...)
return t.Clean()
}

Expand Down Expand Up @@ -147,10 +156,10 @@ func (e *Elder) GoBuild(appName string, opts ...taskGoBuild.Option) error {
func (e *Elder) Gofumpt(opts ...taskGofumpt.Option) error {
t, tErr := taskGofumpt.New(opts...)
if tErr != nil {
return tErr
return fmt.Errorf(`create "gofumpt" task: %w`, tErr)
}

return e.gobinRunner.Run(t)
return e.goToolRunner.Run(t)
}

// Goimports is a task for the "golang.org/x/tools/cmd/goimports" Go module command.
Expand All @@ -165,10 +174,10 @@ func (e *Elder) Gofumpt(opts ...taskGofumpt.Option) error {
func (e *Elder) Goimports(opts ...taskGoimports.Option) error {
t, tErr := taskGoimports.New(opts...)
if tErr != nil {
return tErr
return fmt.Errorf(`create "goimports" task: %w`, tErr)
}

return e.gobinRunner.Run(t)
return e.goToolRunner.Run(t)
}

// GolangCILint is a task to run the "github.com/golangci/golangci-lint/cmd/golangci-lint" Go module
Expand All @@ -185,10 +194,10 @@ func (e *Elder) Goimports(opts ...taskGoimports.Option) error {
func (e *Elder) GolangCILint(opts ...taskGolangCILint.Option) error {
t, tErr := taskGolangCILint.New(opts...)
if tErr != nil {
return tErr
return fmt.Errorf(`create "golangci-lint" task: %w`, tErr)
}

return e.gobinRunner.Run(t)
return e.goToolRunner.Run(t)
}

// GoTest is a task to run the Go toolchain "test" command.
Expand Down Expand Up @@ -233,10 +242,10 @@ func (e *Elder) Gox(appName string, opts ...taskGox.Option) error {

t, tErr := taskGox.New(ac, opts...)
if tErr != nil {
return tErr
return fmt.Errorf(`create "gox" task: %w`, tErr)
}

return e.gobinRunner.Run(t)
return e.goToolRunner.Run(t)
}

// RegisterApp creates and stores a new application configuration.
Expand Down Expand Up @@ -289,10 +298,10 @@ func (e *Elder) RegisterApp(name, displayName, pathRel string) error {
return nil
}

// Validate ensures that all tasks are properly initialized and operational.
// Validate ensures that the wand is properly initialized and operational.
// It returns an error of type *task.ErrRunner when the validation of any of the supported task fails.
func (e *Elder) Validate() error {
for _, t := range []task.Runner{e.goRunner, e.gobinRunner} {
for _, t := range []task.Runner{e.goRunner} {
if err := t.Validate(); err != nil {
return fmt.Errorf("failed to validate runner: %w", err)
}
Expand All @@ -305,7 +314,8 @@ func (e *Elder) Validate() error {
//
// The module name is determined automatically using the "runtime/debug" package.
// The absolute path to the root directory is automatically set based on the current working directory.
// When the WithGenWandDataDir option is set to `true` the directory for wand specific data will be auto-generated.
// When the WithDisableAutoGenWandDataDir option is set to `false` the auto-generation of the directory for wand
// specific data will be disabled.
// Note that the working directory must be set manually when the "magefile" is not placed in the root directory by
// pointing Mage to it:
// - "-d <PATH>" option to set the directory from which "magefiles" are read (defaults to ".").
Expand Down Expand Up @@ -336,11 +346,23 @@ func New(opts ...Option) (*Elder, error) {

e.goRunner = taskGo.NewRunner(e.opts.goRunnerOpts...)

gobinRunner, gobinRunnerErr := taskGobin.NewRunner(e.opts.gobinRunnerOpts...)
if gobinRunnerErr != nil {
return nil, fmt.Errorf("failed to create %q runner: %w", "gobin", gobinRunnerErr)
goToolRunnerOpts := append(
[]taskGoTool.RunnerOption{
taskGoTool.WithToolsBinDir(filepath.Join(e.project.Options().WandDataDir, DefaultGoToolsBinDir)),
},
e.opts.goToolRunnerOpts...,
)
goToolRunner, goToolRunnerErr := taskGoTool.NewRunner(e.goRunner, goToolRunnerOpts...)
if goToolRunnerErr != nil {
return nil, fmt.Errorf("create %q runner: %w", taskGoTool.RunnerName, goToolRunnerErr)
}
e.goToolRunner = goToolRunner

if !e.opts.disableAutoGenWandDataDir {
if err := generateWandDataDir(e.project.Options().WandDataDir); err != nil {
return nil, fmt.Errorf("generate wand specific data directory %q: %w", e.project.Options().WandDataDir, err)
}
}
e.gobinRunner = gobinRunner

if err := e.RegisterApp(e.project.Options().Name, e.project.Options().DisplayName, project.AppRelPath); err != nil {
e.ExitPrintf(1, nib.ErrorVerbosity, "registering application %q: %v", e.project.Options().Name, err)
Expand Down
2 changes: 2 additions & 0 deletions pkg/elder/gitignore.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Do not track cached data like compiled executables of Go module-based "main" packages.
cache/
Loading

0 comments on commit 7e77e44

Please sign in to comment.