package dependencies import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "github.com/blang/semver" "github.com/juju/errors" "git.dev.m-and-m.ovh/mderasse/gouick/helpers" "git.dev.m-and-m.ovh/mderasse/gouick/models" log "github.com/sirupsen/logrus" ) // minimumGolangVersion is the minimum minor required for the app to work. const minimumGolangVersion = "1.19.3" // installation directory for fresh install. // will be prefixed by $HOME. const defaultGolangInstallDir = "/local/go" var regexGolangVersion = regexp.MustCompile(`^go version go(\d+\.\d+\.\d+)`) // Golang represent an empty struct respecting the DependencyInterface. type Golang struct{} // regroup all golang dependencies function. // GetName return the name of the dependency using DependencyName enum. func (g Golang) GetName() models.DependencyName { return models.DependencyName_GOLANG } // GetMinimumVersion return the minimum version required for that dependency. func (g Golang) GetMinimumVersion() string { return minimumGolangVersion } // IsInstalled check if the dependency is installed on the system. func (g Golang) IsInstalled() (bool, error) { _, err := g.GetBinaryPath() if err != nil && !errors.Is(err, exec.ErrNotFound) { return false, errors.Trace(err) } else if err != nil && errors.Is(err, exec.ErrNotFound) { return false, nil } return true, nil } // GetBinaryPath will search for the binary and return the path if found. func (g Golang) GetBinaryPath() (string, error) { log.Debug("looking for golang binary") path, err := exec.LookPath("go") if err != nil { return "", errors.Trace(err) } log.Debug("found golang binary in", path) return path, nil } // GetVersion will find the current version of the dependency. func (g Golang) GetVersion() (string, error) { isInstalled, err := g.IsInstalled() if err != nil { return "", errors.Trace(err) } if !isInstalled { return "", errors.NotFoundf("golang is not installed on the system") } binaryPath, err := g.GetBinaryPath() if err != nil { return "", errors.Trace(err) } log.Debug("executing go version command") //nolint:gosec // we trust the binary we are launching cmd := exec.Command(binaryPath, "version") stdout, err := cmd.Output() if err != nil { return "", errors.Trace(err) } cleanOut := strings.TrimSpace(string(stdout)) log.Debugf("go version returned %s", cleanOut) parseOutput := regexGolangVersion.FindStringSubmatch(cleanOut) if len(parseOutput) != 2 { return "", errors.NotSupportedf("failed to parse golang version output: %s", cleanOut) } return parseOutput[1], nil } // IsVersionSupported will compare the current version with the minimum expected version. func (g Golang) IsVersionSupported() (bool, error) { isInstalled, err := g.IsInstalled() if err != nil { return false, errors.Trace(err) } if !isInstalled { return false, nil } version, err := g.GetVersion() if err != nil { return false, errors.Trace(err) } installedVersion, err := semver.Make(version) if err != nil { return false, errors.Trace(err) } requiredVersion, _ := semver.Make(minimumGolangVersion) if err != nil { return false, errors.Trace(err) } if installedVersion.LT(requiredVersion) { return false, nil } return true, nil } // CanBeInstalled define if the dependency installation is handled by the app. func (g Golang) CanBeInstalled() bool { return true } // DescribeInstall will list the aciton that will be executed for the dependency update or installation. func (g Golang) DescribeInstall(path string) string { commands := []string{ "The following commands will be executed", fmt.Sprintf("rm -rf %s/* ", path), fmt.Sprintf("curl %s | tar --strip-components=1 -C %s -zxf -", g.getDownloadURL(), path), } return strings.Join(commands, "\n") } // Install will install or update the dependency. func (g Golang) Install(path string) error { err := helpers.RemoveDirectoryContent(path) if err != nil { log.Warnf("fail to delete content of directory %s", path) } downloadURL := g.getDownloadURL() content, err := downloadFile(downloadURL) if err != nil { log.Warnf("fail to download file from %s", downloadURL) return errors.Trace(err) } // zip on windows, tar gz on other platform if runtime.GOOS == "windows" { log.Debug("Working on zip, Unzip") err = unZip(content, "go/", path) if err != nil { log.Warnf("fail to un-zip downloaded file from %s", downloadURL) return errors.Trace(err) } } else { log.Debug("Working on tar gz, UnGzip & unTar") gzipReader, err := unGzip(content) if err != nil { log.Warnf("fail to un-gzip downloaded file from %s, error:", downloadURL) return errors.Trace(err) } err = unTar(gzipReader, "go/", path) if err != nil { log.Warnf("fail to un-tar downloaded file from %s", downloadURL) return errors.Trace(err) } } return nil } // DescribePostInstall will list the post installation action that will be executed. func (g Golang) DescribePostInstall(path string) string { descriptions := []string{ `For your environment to work correctly, we will add if needed the following environment variable to your .bashrc: export GOPATH="$HOME/go"`, fmt.Sprintf(" export GOROOT=\"%s\"", path), ` PATH="$GOROOT/bin:$GOPATH/bin:$PATH" You will have to reopen a new terminal to apply the changes or execute the following command: source $HOME/.bashrc `, } return strings.Join(descriptions, "\n") } // PostInstall will execute the post installation or update of the dependency. func (g Golang) PostInstall(path string) error { if runtime.GOOS == "windows" { log.Warnf("Unable to environement variable on windows. Please add it by yourself") return nil } lineBashRc := []string{ "# Golang - Added by gouick", } createGoPath := false if os.Getenv("GOPATH") == "" { lineBashRc = append(lineBashRc, "export GOPATH=\"$HOME/go\"") createGoPath = true } if os.Getenv("GOROOT") == "" { lineBashRc = append(lineBashRc, fmt.Sprintf("export GOROOT=\"%s\"", path)) } // checking if go binary is found after installation // if not it's probably not in the path isInstalled, err := g.IsInstalled() if err != nil { return errors.Trace(err) } if !isInstalled { lineBashRc = append(lineBashRc, "PATH=\"$GOROOT/bin:$GOPATH/bin:$PATH\"") } homeDir, err := os.UserHomeDir() if err != nil { return errors.Trace(err) } if len(lineBashRc) > 1 { log.Debug("Adding env variable to .bashrc") //nolint:gosec // we did compute the file path fh, err := os.OpenFile( filepath.Join(homeDir, ".bashrc"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600, ) if err != nil { return errors.Trace(err) } defer func() { if err := fh.Close(); err != nil { log.Errorf("Error closing file: %s", err) } }() _, err = fh.WriteString( fmt.Sprintf("\n\n%s\n", strings.Join(lineBashRc, "\n")), ) if err != nil { return errors.Trace(err) } } if createGoPath { log.Debug("creating gopath directory") _, err = helpers.CheckAndCreateDir(filepath.Join(homeDir, "go")) if err != nil { return errors.Trace(err) } } return nil } // GetInstallDirectory will try to find the current golang directory. If it doesn't exist or if it's in a // not userspace directory, it will provide the "default" // It doesn't mean that the directory is "writable". func (g Golang) GetInstallDirectory() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", errors.Trace(err) } isInstalled, err := g.IsInstalled() if err != nil { return "", errors.Trace(err) } // concat default install dir with home and we have our path if !isInstalled { return filepath.Join(homeDir, defaultGolangInstallDir), nil } // now let's play and find the current install path binaryPath, err := g.GetBinaryPath() if err != nil { return "", errors.Trace(err) } log.Debug("executing go env GOROOT command") //nolint:gosec // we trust the binary we are launching cmd := exec.Command(binaryPath, "env", "GOROOT") stdout, err := cmd.Output() if err != nil { return "", errors.Trace(err) } cleanOut := strings.TrimSpace(string(stdout)) if !strings.Contains(cleanOut, homeDir) { return filepath.Join(homeDir, defaultGolangInstallDir), nil } return cleanOut, nil } func (g Golang) getDownloadURL() string { if runtime.GOOS == "windows" { return fmt.Sprintf("https://dl.google.com/go/go%s.%s-%s.zip", minimumGolangVersion, runtime.GOOS, runtime.GOARCH) } return fmt.Sprintf("https://dl.google.com/go/go%s.%s-%s.tar.gz", minimumGolangVersion, runtime.GOOS, runtime.GOARCH) }