From e435d33eb3203d9009a845a4bc3d0f36b42ef0f8 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 1 Feb 2024 22:53:22 +0100 Subject: [PATCH] feat(buildtool): add the gofixpath subcommand (#1484) This subcommand executes another command ensuring a PATH lookup for this subcommand resolves "go" as the version of Go specified in the `GOVERSION` file. This is a building block for https://github.com/ooni/probe/issues/2664. --- .github/workflows/gofixpath.yml | 32 ++++++++++ internal/cmd/buildtool/gofixpath.go | 76 +++++++++++++++++++++++ internal/cmd/buildtool/golang.go | 13 +++- internal/cmd/buildtool/golang_test.go | 18 +++++- internal/cmd/buildtool/main.go | 1 + internal/cmd/buildtool/testdata/GOVERSION | 1 + 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/gofixpath.yml create mode 100644 internal/cmd/buildtool/gofixpath.go create mode 100644 internal/cmd/buildtool/testdata/GOVERSION diff --git a/.github/workflows/gofixpath.yml b/.github/workflows/gofixpath.yml new file mode 100644 index 0000000000..ddf093acdf --- /dev/null +++ b/.github/workflows/gofixpath.yml @@ -0,0 +1,32 @@ +# Ensures that ./internal/cmd/buildtool gofixpath {command} [arguments] downloads the correct +# version of go and executes {command} with [arguments] with "go" being the right version. + +name: gofixpath +on: + push: + branches: + - "release/**" + - "fullbuild" + - "gofixpathbuild" + - "master" + tags: + - "v*" + pull_request: + schedule: + - cron: "17 7 * * *" + +jobs: + build_with_specific_go_version: + strategy: + matrix: + goversion: ["1.19", "1.20", "1.21"] # when releasing check whether we need to update this array + system: [ubuntu-latest, macos-latest] + runs-on: "${{ matrix.system }}" + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-go@v4 + with: + go-version: "${{ matrix.goversion }}" + + - run: go run ./internal/cmd/buildtool gofixpath -- go run ./internal/cmd/buildtool generic miniooni diff --git a/internal/cmd/buildtool/gofixpath.go b/internal/cmd/buildtool/gofixpath.go new file mode 100644 index 0000000000..a5e1053b83 --- /dev/null +++ b/internal/cmd/buildtool/gofixpath.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/cmd/buildtool/internal/buildtoolmodel" + "github.com/ooni/probe-cli/v3/internal/must" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/shellx" + "github.com/spf13/cobra" +) + +// gofixpathSubcommand returns the gofixpath [cobra.Command]. +func gofixpathSubcommand() *cobra.Command { + return &cobra.Command{ + Use: "gofixpath", + Short: "Executes a command ensuring the expected version of Go comes first in PATH lookup", + Run: func(cmd *cobra.Command, args []string) { + gofixpathMain(&buildDeps{}, args...) + }, + Args: cobra.MinimumNArgs(1), + } +} + +// gofixpathMain ensures the correct version of Go is in path, otherwise +// installs such a version, configure the PATH correctly, and then executes +// whatever argument passed to the command with the correct PATH. +func gofixpathMain(deps buildtoolmodel.Dependencies, args ...string) { + // create empty environment + envp := &shellx.Envp{} + + // install and configure the correct go version if needed + if !golangCorrectVersionCheckP("GOVERSION") { + // read the version of Go we would like to use + expected := string(must.FirstLineBytes(must.ReadFile("GOVERSION"))) + + // install the wrapper command + packageName := fmt.Sprintf("golang.org/dl/go%s@latest", expected) + must.Run(log.Log, "go", "install", "-v", packageName) + + // run the wrapper to download the distribution + gobinproxy := filepath.Join( + string(must.FirstLineBytes(must.RunOutput(log.Log, "go", "env", "GOPATH"))), + "bin", + fmt.Sprintf("go%s", expected), + ) + must.Run(log.Log, gobinproxy, "download") + + // add the path to the SDK binary dir + // + // Note: because gomobile wants to execute "go" we must provide the + // path to a directory that contains a command named "go" and we cannot + // just use the gobinproxy binary + sdkbinpath := filepath.Join( + string(must.FirstLineBytes(must.RunOutput(log.Log, gobinproxy, "env", "GOROOT"))), + "bin", + ) + + // prepend to PATH + envp.Append("PATH", cdepsPrependToPath(sdkbinpath)) + } + + // create shellx configuration + config := &shellx.Config{ + Logger: log.Log, + Flags: shellx.FlagShowStdoutStderr, + } + + // create argv + argv := runtimex.Try1(shellx.NewArgv(args[0], args[1:]...)) // safe because cobra.MinimumNArgs(1) + + // execute the child command + runtimex.Try0(shellx.RunEx(config, argv, envp)) +} diff --git a/internal/cmd/buildtool/golang.go b/internal/cmd/buildtool/golang.go index e086534708..7365395f04 100644 --- a/internal/cmd/buildtool/golang.go +++ b/internal/cmd/buildtool/golang.go @@ -12,16 +12,23 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) -// golangCheck checks whether the "go" binary is the correct version -func golangCheck(filename string) { +// golangCorrectVersionCheckP returns whether we're using the correct golang version. +func golangCorrectVersionCheckP(filename string) bool { expected := string(must.FirstLineBytes(must.ReadFile(filename))) firstline := string(must.FirstLineBytes(must.RunOutput(log.Log, "go", "version"))) vec := strings.Split(firstline, " ") runtimex.Assert(len(vec) == 4, "expected four tokens") if got := vec[2]; got != "go"+expected { - log.Fatalf("expected go%s but got %s", expected, got) + log.Warnf("expected go%s but got %s", expected, got) + return false } log.Infof("using go%s", expected) + return true +} + +// golangCheck checks whether the "go" binary is the correct version +func golangCheck(filename string) { + runtimex.Assert(golangCorrectVersionCheckP(filename), "invalid Go version") } // golangGOPATH returns the GOPATH value. diff --git a/internal/cmd/buildtool/golang_test.go b/internal/cmd/buildtool/golang_test.go index 5f64e678f0..90cf6c6be0 100644 --- a/internal/cmd/buildtool/golang_test.go +++ b/internal/cmd/buildtool/golang_test.go @@ -6,6 +6,20 @@ import ( ) func TestGolangCheck(t *testing.T) { - // make sure the code does not panic when it runs - golangCheck(filepath.Join("..", "..", "..", "GOVERSION")) + t.Run("successful case using the correct go version", func(t *testing.T) { + golangCheck(filepath.Join("..", "..", "..", "GOVERSION")) + }) + + t.Run("invalid Go version where we expect a panic", func(t *testing.T) { + var panicked bool + func() { + defer func() { + panicked = recover() != nil + }() + golangCheck(filepath.Join("testdata", "GOVERSION")) + }() + if !panicked { + t.Fatal("should have panicked") + } + }) } diff --git a/internal/cmd/buildtool/main.go b/internal/cmd/buildtool/main.go index 1012a4b68c..139934b2ec 100644 --- a/internal/cmd/buildtool/main.go +++ b/internal/cmd/buildtool/main.go @@ -20,6 +20,7 @@ func main() { root.AddCommand(androidSubcommand()) root.AddCommand(darwinSubcommand()) root.AddCommand(genericSubcommand()) + root.AddCommand(gofixpathSubcommand()) root.AddCommand(iosSubcommand()) root.AddCommand(linuxSubcommand()) root.AddCommand(oohelperdSubcommand()) diff --git a/internal/cmd/buildtool/testdata/GOVERSION b/internal/cmd/buildtool/testdata/GOVERSION new file mode 100644 index 0000000000..b8f1e3fd3e --- /dev/null +++ b/internal/cmd/buildtool/testdata/GOVERSION @@ -0,0 +1 @@ +1.17.11