@@ -38,12 +38,14 @@ import (
3838
3939// buildDependency represents a build dependency like a tool used to build or develop the project.
4040type buildDependency struct {
41- // The path of the binary executable.
41+ // BinaryExecPath is the path of the binary executable.
4242 BinaryExecPath string
43- // The name of the binary.
43+ // BinaryName is the name of the binary.
4444 BinaryName string
45- // The name of the package.
46- PackageName string
45+ // ModuleName is the name of the module.
46+ ModuleName string
47+ // ModuleVersion is the version of the module including prefixes like "v" if any.
48+ ModuleVersion string
4749}
4850
4951const (
@@ -83,14 +85,33 @@ var (
8385 // See https://github.com/mitchellh/gox for more details.
8486 crossCompileTool = & buildDependency {
8587 BinaryName : "gox" ,
86- PackageName :
"github.com/mitchellh/[email protected] " ,
88+ ModuleName : "github.com/mitchellh/gox" ,
89+ ModuleVersion : "v1.0.1" ,
90+ }
91+
92+ // devToolManager is the tool to install and run all used project tools and applications with Go's module mode.
93+ // This is necessary because the Go toolchain currently doesn't support the handling of local or global project tool
94+ // dependencies in module mode without "polluting" the project's Go module file (go.mod).
95+ //
96+ // See the FAQ/documentations of "gobin" as well as issue references for more details about the tool and its purpose:
97+ // https://github.com/myitcv/gobin/wiki/FAQ
98+ //
99+ // For more details about the status of proposed official Go toolchain solutions and workarounds see the following
100+ // references:
101+ // - https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
102+ // - https://github.com/golang/go/issues/27653
103+ // - https://github.com/golang/go/issues/25922
104+ devToolManager = & buildDependency {
105+ BinaryName : "gobin" ,
106+ ModuleName : "github.com/myitcv/gobin" ,
107+ ModuleVersion : "v0.0.13" ,
87108 }
88109
89110 // The tool used to format all Go source files.
90111 // See https://godoc.org/golang.org/x/tools/cmd/goimports for more details.
91112 formatTool = & buildDependency {
92- PackageName : "golang.org/x/tools/cmd/goimports" ,
93113 BinaryName : "goimports" ,
114+ ModuleName : "golang.org/x/tools/cmd/goimports" ,
94115 }
95116
96117 // Arguments for the `-gcflags` flag to pass on each `go tool compile` invocation.
@@ -110,8 +131,9 @@ var (
110131 // This is the same tool used by the https://golangci.com service that is also integrated in snowsaw's CI/CD pipeline.
111132 // See https://github.com/golangci/golangci-lint for more details.
112133 lintTool = & buildDependency {
113- PackageName :
"github.com/golangci/golangci-lint/cmd/[email protected] " ,
114134 BinaryName : "golangci-lint" ,
135+ ModuleName : "github.com/golangci/golangci-lint/cmd/golangci-lint" ,
136+ ModuleVersion : "v1.19.1" ,
115137 }
116138
117139 // The output directory for reports like test coverage.
@@ -149,9 +171,14 @@ func init() {
149171 goPath = value
150172}
151173
174+ // Bootstrap bootstraps the local development environment by installing the required tools and build dependencies.
175+ func Bootstrap () {
176+ mg .SerialDeps (bootstrap )
177+ }
178+
152179// Build compiles the project in development mode for the current OS and architecture type.
153180func Build () {
154- mg .SerialDeps (Clean , compile )
181+ mg .SerialDeps (clean , compile )
155182}
156183
157184// Clean removes previous development and distribution builds from the project root.
@@ -164,42 +191,42 @@ func Clean() {
164191// version information via LDFLAGS.
165192// Run `strings <PATH_TO_BINARY> | grep "$PWD"` to verify that all paths have been successfully stripped.
166193func Dist () {
167- mg .SerialDeps (Clean , validateBuildDependencies , compileProd )
194+ mg .SerialDeps (validateDevTools , clean , compileProd )
168195}
169196
170197// DistCrossPlatform builds the project in production mode for cross-platform distribution.
171198// This includes all steps from the current platform distribution/production task `Dist`,
172199// but instead builds for all configured OS/architecture types.
173200func DistCrossPlatform () {
174- mg .SerialDeps (Clean , validateBuildDependencies , compileProdCross )
201+ mg .SerialDeps (validateDevTools , clean , compileProdCross )
175202}
176203
177204// DistCrossPlatformOpt builds the project in production mode for cross-platform distribution with optimizations.
178205// This includes all steps from the cross-platform distribution task `DistCrossPlatform` and additionally removes all
179206// debug metadata to shrink the memory overhead and file size as well as reducing the chance for possible security
180207// related problems due to enabled development features and leaked debug information.
181208func DistCrossPlatformOpt () {
182- mg .SerialDeps (Clean , validateBuildDependencies , compileProdCrossOpt )
209+ mg .SerialDeps (validateDevTools , clean , compileProdCrossOpt )
183210}
184211
185212// DistOpt builds the project in production mode with optimizations like minification and debug symbol stripping.
186213// This includes all steps from the production build task `Dist` and additionally removes all debug metadata to shrink
187214// the memory overhead and file size as well as reducing the chance for possible security related problems due to
188215// enabled development features and leaked debug information.
189216func DistOpt () {
190- mg .SerialDeps (Clean , validateBuildDependencies , compileProdOpt )
217+ mg .SerialDeps (validateDevTools , clean , compileProdOpt )
191218}
192219
193220// Format searches all project Go source files and formats them according to the Go code styleguide.
194221func Format () {
195- mg .SerialDeps (validateBuildDependencies , runGoImports )
222+ mg .SerialDeps (validateDevTools , runGoImports )
196223}
197224
198225// Lint runs all linters configured and executed through `golangci-lint`.
199226// See the `.golangci.yml` configuration file and official GolangCI documentations at https://golangci.com
200227// and https://github.com/golangci/golangci-lint for more details.
201228func Lint () {
202- mg .SerialDeps (validateBuildDependencies , runGolangCILint )
229+ mg .SerialDeps (validateDevTools , runGolangCILint )
203230}
204231
205232// Test runs all unit tests with enabled race detection.
@@ -209,7 +236,7 @@ func Test() {
209236
210237// TestCover runs all unit tests with with coverage reports and enabled race detection.
211238func TestCover () {
212- mg .SerialDeps (Clean )
239+ mg .SerialDeps (clean )
213240 // Ensure the required directory structure exists, `go test` doesn't create it automatically.
214241 createDirectoryStructure (reportsDir )
215242 testCoverageProfileFlag = fmt .Sprintf ("-coverprofile=%s" , filepath .Join (reportsDir , testCoverageOutputFileName ))
@@ -221,6 +248,55 @@ func TestIntegration() {
221248 mg .SerialDeps (integrationTests )
222249}
223250
251+ func bootstrap () {
252+ prt .Infof ("Bootstrapping development tool/dependency manager %s" ,
253+ color .CyanString ("%s@%s" , devToolManager .ModuleName , devToolManager .ModuleVersion ))
254+ cmdInstallGobin := exec .Command (goExec , "get" , "-u" ,
255+ fmt .Sprintf ("%s@%s" , devToolManager .ModuleName , devToolManager .ModuleVersion ))
256+ // Run the installation outside of the project root directory to prevent the pollution of the project's Go module
257+ // file.
258+ // This is a necessary workaround until the Go toolchain is able to install packages globally without
259+ // updating the module file when the "go get" command is run from within the project root directory.
260+ // See https://github.com/golang/go/issues/30515 for more details or more details and proposed solutions
261+ // that might be added to Go's build tools in future versions.
262+ cmdInstallGobin .Dir = os .TempDir ()
263+ cmdInstallGobin .Env = os .Environ ()
264+ // Explicitly enable "module" mode when installing the dev tool manager to allow to use pinned module version.
265+ cmdInstallGobin .Env = append (cmdInstallGobin .Env , "GO111MODULE=on" )
266+ if gobinInstallErr := cmdInstallGobin .Run (); gobinInstallErr != nil {
267+ prt .Errorf ("Failed to install required development tool/dependency manager %s:\n %s" ,
268+ color .CyanString ("%s@%s" , devToolManager .ModuleName , devToolManager .ModuleVersion ),
269+ color .RedString ("%s" , gobinInstallErr ))
270+ os .Exit (1 )
271+ }
272+
273+ prt .Infof ("Bootstrapping required development tools/dependencies:" )
274+ for _ , bd := range []* buildDependency {crossCompileTool , formatTool , lintTool } {
275+ modulePath := bd .ModuleName
276+ // If the non-module dependency is not installed yet, install it normally into the $GOBIN path,...
277+ if bd .ModuleVersion == "" {
278+ fmt .Println (color .CyanString (" %s" , modulePath ))
279+ if installErr := sh .Run (devToolManager .BinaryName , "-u" , modulePath ); installErr != nil {
280+ prt .Errorf ("Failed to install required development tool/dependency %s:\n %s" ,
281+ color .CyanString (modulePath ), color .RedString ("%s" , installErr ))
282+ os .Exit (1 )
283+ }
284+ continue
285+ }
286+
287+ // ...otherwise install into "gobin" binary cache.
288+ modulePath = fmt .Sprintf ("%s@%s" , bd .ModuleName , bd .ModuleVersion )
289+ fmt .Println (color .CyanString (" %s" , modulePath ))
290+ if installErr := sh .Run (devToolManager .BinaryName , "-u" , modulePath ); installErr != nil {
291+ prt .Errorf ("Failed to install required development tool/dependency %s:\n %s" ,
292+ color .CyanString (modulePath ), color .RedString ("%s" , installErr ))
293+ os .Exit (1 )
294+ }
295+ }
296+
297+ prt .Successf ("Successfully bootstrapped required development tools/dependencies" )
298+ }
299+
224300func clean () {
225301 if err := os .RemoveAll (buildDir ); err != nil {
226302 prt .Errorf ("Failed to clean up project directory: %v" , err )
@@ -326,17 +402,17 @@ func getEnvFlags() map[string]string {
326402 "Injecting %s:\n " +
327403 " Build Date: %s\n " +
328404 " Version: %s" ,
329- color .CyanString ("LDFLAGS" ), color .CyanString (buildDate ), color .CyanString (strings .Join (version , "-" )))
405+ color .BlueString ("LDFLAGS" ), color .CyanString (buildDate ), color .CyanString (strings .Join (version , "-" )))
330406
331407 prt .Infof (
332408 "Injecting %s:\n " +
333409 " -trimpath: %s" ,
334- color .CyanString ("ASMFLAGS" ), color .CyanString (pwd ))
410+ color .BlueString ("ASMFLAGS" ), color .CyanString (pwd ))
335411
336412 prt .Infof (
337413 "Injecting %s:\n " +
338414 " -trimpath: %s" ,
339- color .CyanString ("GCFLAGS" ), color .CyanString (pwd ))
415+ color .BlueString ("GCFLAGS" ), color .CyanString (pwd ))
340416
341417 return map [string ]string {
342418 "BUILD_DATE_TIME" : buildDate ,
@@ -345,6 +421,15 @@ func getEnvFlags() map[string]string {
345421 "VERSION" : strings .Join (version , "-" )}
346422}
347423
424+ // getExecutablePath returns the path to the executable for the given package/module.
425+ // When the "resolveWithGobin" parameter is set to true, the path will be resolved from the "gobin" binary cache.
426+ func getExecutablePath (name string , resolveWithGobin bool ) (string , error ) {
427+ if resolveWithGobin {
428+ return sh .Output (devToolManager .BinaryName , "-p" , "-nonet" , name )
429+ }
430+ return exec .LookPath (name )
431+ }
432+
348433// prepareBuildTags reads custom build tags defined by the user through the `SNOWSAW_BUILD_TAGS` environment
349434// variable and appends them together with all additionally passed tags to the global `tags` slice.
350435// Returns `true` if custom build tags have been loaded, `false` otherwise.
@@ -461,50 +546,37 @@ func runGox(envFlags map[string]string, buildFlags ...string) {
461546 prt .Successf ("Cross compilation completed successfully with output to %s directory" , color .GreenString (buildDir ))
462547}
463548
464- // validateBuildDependencies checks if all required build dependencies are installed, the binaries are available in
465- // PATH and will try to install them if not passing the checks.
466- func validateBuildDependencies () {
549+ // validateDevTools validates that all required development tool/dependency executables are bootstrapped and
550+ // available in PATH or "gobin" binary cache.
551+ func validateDevTools () {
552+ prt .Infof ("Verifying development tools/dependencies" )
553+ handleError := func (name string , err error ) {
554+ prt .Errorf ("Failed do determine development tool/dependency %s:\n %s" ,
555+ color .CyanString (name ), color .RedString (" %s" , err ))
556+ prt .Warnf ("Run the %s task to install all required tools/dependencies!" , color .YellowString ("bootstrap" ))
557+ os .Exit (1 )
558+ }
559+
560+ gobinPath , checkGobinPathErr := getExecutablePath (devToolManager .BinaryName , false )
561+ if checkGobinPathErr != nil {
562+ handleError (fmt .Sprintf ("%s@%s" , devToolManager .ModuleName , devToolManager .ModuleVersion ), checkGobinPathErr )
563+ }
564+ devToolManager .BinaryExecPath = gobinPath
565+
467566 for _ , bd := range []* buildDependency {crossCompileTool , formatTool , lintTool } {
468- binPath , err := exec . LookPath ( bd . BinaryName )
469- if err == nil {
470- bd . BinaryExecPath = binPath
471- prt . Infof ( "Required build dependency %s already installed: %s" ,
472- color . CyanString ( bd . PackageName ),
473- color . BlueString ( bd .BinaryExecPath ))
567+ if bd . ModuleVersion == "" {
568+ p , e := getExecutablePath ( bd . BinaryName , false )
569+ if e != nil {
570+ handleError ( bd . ModuleName , e )
571+ }
572+ bd .BinaryExecPath = p
474573 continue
475574 }
476575
477- prt .Infof ("Installing required build dependency: %s" , color .CyanString (bd .PackageName ))
478- c := exec .Command (goExec , "get" , "-u" , bd .PackageName )
479- // Run installations outside of the project root directory to prevent the pollution of the project's Go module
480- // file.
481- // This is a necessary workaround until the Go toolchain is able to install packages globally without
482- // updating the module file when the "go get" command is run from within the project root directory.
483- // See https://github.com/golang/go/issues/30515 for more details or more details and proposed solutions
484- // that might be added to Go's build tools in future versions.
485- c .Dir = os .TempDir ()
486- c .Env = os .Environ ()
487- // Explicitly enable "module" mode to install development dependencies to allow to use pinned module versions.
488- env := map [string ]string {"GO111MODULE" : "on" }
489- for k , v := range env {
490- c .Env = append (c .Env , k + "=" + v )
491- }
492- if err = c .Run (); err != nil {
493- prt .Errorf ("Failed to install required build dependency %s: %v" , color .CyanString (bd .PackageName ), err )
494- prt .Warnf ("Please install manually: %s" , color .CyanString ("go get -u %s" , bd .PackageName ))
495- os .Exit (1 )
496- }
497-
498- binPath , err = exec .LookPath (bd .BinaryName )
499- if err != nil {
500- bd .BinaryExecPath = binPath
501- prt .Errorf ("Failed to find executable path of required build dependency %s after installation: %v" ,
502- color .CyanString (bd .PackageName ), err )
503- os .Exit (1 )
576+ p , e := getExecutablePath (fmt .Sprintf ("%s@%s" , bd .ModuleName , bd .ModuleVersion ), true )
577+ if e != nil {
578+ handleError (fmt .Sprintf ("%s@%s" , bd .ModuleName , bd .ModuleVersion ), e )
504579 }
505- bd .BinaryExecPath = binPath
506- prt .Infof ("Using executable %s of installed build dependency %s" ,
507- color .CyanString (bd .BinaryExecPath ),
508- color .BlueString (bd .PackageName ))
580+ bd .BinaryExecPath = p
509581 }
510582}
0 commit comments