github.com/jonsyu1/godel@v0.0.0-20171017211503-64567a0cf169/cmd/checkpath/checkpath.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package checkpath
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"os/exec"
    22  	"path"
    23  	"regexp"
    24  	"runtime"
    25  	"strings"
    26  	"syscall"
    27  
    28  	"github.com/pkg/errors"
    29  	"github.com/termie/go-shutil"
    30  )
    31  
    32  func VerifyProject(wd string, apply bool, stdout io.Writer) error {
    33  	gopath := os.Getenv("GOPATH")
    34  	if gopath == "" {
    35  		return errors.Errorf("GOPATH environment variable must be set")
    36  	}
    37  
    38  	goroot := os.Getenv("GOROOT")
    39  	if goroot == "" {
    40  		// GOROOT is not set as environment variable: check value of GOROOT provided by compiled code
    41  		compiledGoRoot := runtime.GOROOT()
    42  		if goRootPathInfo, err := os.Stat(compiledGoRoot); err != nil || !goRootPathInfo.IsDir() {
    43  			// compiled GOROOT does not exist: get GOROOT from "go env GOROOT"
    44  			goEnvRoot := goEnvRoot()
    45  
    46  			fmt.Fprintln(stdout, "GOROOT environment variable is not set and the value provided in the compiled code does not exist locally.")
    47  
    48  			if goEnvRoot != "" {
    49  				shellCfgFiles := getShellCfgFiles()
    50  				if !apply {
    51  					fmt.Fprintf(stdout, "'go env' reports that GOROOT is %s. Suggested fix:\n", goEnvRoot)
    52  					fmt.Fprintf(stdout, "  export GOROOT=%s\n", goEnvRoot)
    53  					for _, currCfgFile := range shellCfgFiles {
    54  						fmt.Fprintf(stdout, "  echo \"export GOROOT=%s\" >> %q \n", goEnvRoot, currCfgFile)
    55  					}
    56  				} else {
    57  					for _, currCfgFile := range shellCfgFiles {
    58  						fmt.Fprintf(stdout, "Adding \"export GOROOT=%s\" to %s...\n", goEnvRoot, currCfgFile)
    59  						if err := appendToFile(currCfgFile, fmt.Sprintf("export GOROOT=%v\n", goEnvRoot)); err != nil {
    60  							fmt.Fprintf(stdout, "Failed to add \"export GOROOT=%s\" in %s\n", goEnvRoot, currCfgFile)
    61  						}
    62  					}
    63  				}
    64  			} else {
    65  				fmt.Fprintln(stdout, "Unable to determine GOROOT using 'go env GOROOT'. Ensure that Go was installed properly with source files.")
    66  			}
    67  		}
    68  	}
    69  
    70  	srcPath, err := gitRepoRootPath(wd)
    71  	if err != nil {
    72  		return errors.Errorf("Directory %q must be in a git project", wd)
    73  	}
    74  
    75  	pkgPath, gitRemoteURL, err := gitRemoteOriginPkgPath(wd)
    76  	if err != nil {
    77  		return errors.Errorf("Unable to determine URL of git remote for %s: verify that the project was checked out from a git remote and that the remote URL is set", wd)
    78  	} else if pkgPath == "" {
    79  		return errors.Errorf("Unable to determine the expected package path from git remote URL of %s", gitRemoteURL)
    80  	}
    81  
    82  	dstPath := path.Join(gopath, "src", pkgPath)
    83  	if srcPath == dstPath {
    84  		fmt.Fprintln(stdout, "Project appears to be in the correct location")
    85  		return nil
    86  	}
    87  	dstPathParentDir := path.Dir(dstPath)
    88  
    89  	fmt.Fprintf(stdout, "Project path %q differs from expected path %q\n", srcPath, dstPath)
    90  	if _, err := os.Stat(dstPath); !os.IsNotExist(err) {
    91  		fmt.Fprintf(stdout, "Expected destination path %q already exists.\n", dstPath)
    92  		fmt.Fprintln(stdout, "If this project is known to be the correct one, remove or rename the file or directory at the destination path and run this command again.")
    93  		return nil
    94  	}
    95  
    96  	var fixOperation func(srcPath, dstPath string) error
    97  	var fixOpMessage string
    98  	if pathsOnSameDevice(srcPath, gopath) {
    99  		if !apply {
   100  			fmt.Fprintln(stdout, "Project and GOPATH are on same device. Suggested fix:")
   101  			fmt.Fprintf(stdout, "  mkdir -p %q && mv %q %q\n", dstPathParentDir, srcPath, dstPath)
   102  		}
   103  		fixOpMessage = fmt.Sprintf("Moving %q to %q...", wd, dstPath)
   104  		fixOperation = os.Rename
   105  	} else {
   106  		if !apply {
   107  			fmt.Fprintln(stdout, "Project and GOPATH are on different devices. Suggested fix:")
   108  			fmt.Fprintf(stdout, "  mkdir -p %q && cp -r %q %q\n", dstPathParentDir, srcPath, dstPath)
   109  		}
   110  		fixOpMessage = fmt.Sprintf("Copying %q to %q...", wd, dstPath)
   111  		fixOperation = func(srcPath, dstPath string) error {
   112  			return shutil.CopyTree(srcPath, dstPath, nil)
   113  		}
   114  	}
   115  
   116  	if !apply {
   117  		fmt.Fprintf(stdout, "%s found issues. Run the following godel command to implement suggested fixes:\n", cmd)
   118  		fmt.Fprintf(stdout, "  %s --%s\n", cmd, applyFlag)
   119  	} else {
   120  		if err := os.MkdirAll(dstPathParentDir, 0755); err != nil {
   121  			return errors.Errorf("Failed to create path to %q: %v", dstPathParentDir, err)
   122  		}
   123  		fmt.Fprintln(stdout, fixOpMessage)
   124  		if err := fixOperation(srcPath, dstPath); err != nil {
   125  			return err
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  func goEnvRoot() string {
   132  	if output, err := exec.Command("go", "env", "GOROOT").CombinedOutput(); err == nil {
   133  		return strings.TrimSpace(string(output))
   134  	}
   135  	return ""
   136  }
   137  
   138  func getShellCfgFiles() []string {
   139  	var files []string
   140  	for _, currCfg := range []string{".bash_profile", ".zshrc"} {
   141  		if currFile := checkForCfgFile(currCfg); currFile != "" {
   142  			files = append(files, currFile)
   143  		}
   144  	}
   145  	return files
   146  }
   147  
   148  func appendToFile(filename, content string) (rErr error) {
   149  	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0666)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	defer func() {
   154  		if err := f.Close(); err != nil && rErr == nil {
   155  			rErr = errors.Wrapf(err, "failed to close file %s in defer", filename)
   156  		}
   157  	}()
   158  	if _, err = f.WriteString(content); err != nil {
   159  		return err
   160  	}
   161  	return nil
   162  }
   163  
   164  func checkForCfgFile(name string) string {
   165  	cfgFile := path.Join(os.Getenv("HOME"), name)
   166  	if _, err := os.Stat(cfgFile); err == nil {
   167  		return cfgFile
   168  	}
   169  	return ""
   170  }
   171  
   172  func pathsOnSameDevice(p1, p2 string) bool {
   173  	id1, ok := getDeviceID(p1)
   174  	if !ok {
   175  		return false
   176  	}
   177  	id2, ok := getDeviceID(p2)
   178  	if !ok {
   179  		return false
   180  	}
   181  	return id1 == id2
   182  }
   183  
   184  func getDeviceID(p string) (interface{}, bool) {
   185  	fi, err := os.Stat(p)
   186  	if err != nil {
   187  		return 0, false
   188  	}
   189  	s := fi.Sys()
   190  	switch s := s.(type) {
   191  	case *syscall.Stat_t:
   192  		return s.Dev, true
   193  	}
   194  	return 0, false
   195  }
   196  
   197  func gitRepoRootPath(dir string) (string, error) {
   198  	cmd := exec.Command("git", "rev-parse", "--show-toplevel")
   199  	cmd.Dir = dir
   200  	output, err := cmd.CombinedOutput()
   201  	if err != nil {
   202  		return "", fmt.Errorf("%v failed: could not determine root of git repository for directory %v", cmd.Args, dir)
   203  	}
   204  	return strings.TrimSpace(string(output)), nil
   205  }
   206  
   207  func gitRemoteOriginPkgPath(dir string) (string, string, error) {
   208  	cmd := exec.Command("git", "ls-remote", "--get-url")
   209  	cmd.Dir = dir
   210  	bytes, err := cmd.CombinedOutput()
   211  	if err != nil {
   212  		return "", "", fmt.Errorf("%v failed: git remote URL is not set", cmd.Args)
   213  	}
   214  
   215  	pkgPath := ""
   216  	url := strings.TrimSpace(string(bytes))
   217  	if protocolSeparator := regexp.MustCompile("https?://").FindStringIndex(url); protocolSeparator != nil {
   218  		if protocolSeparator[0] == 0 {
   219  			pkgPath = url[protocolSeparator[1]:]
   220  		}
   221  	} else if atSign := strings.Index(url, "@"); atSign != -1 {
   222  		// assume SSH format of "user@host:org/repo"
   223  		pkgPath = url[atSign+1:]
   224  		pkgPath = strings.Replace(pkgPath, ":", "/", 1)
   225  	}
   226  
   227  	// trim ".git" suffix if present
   228  	if strings.HasSuffix(pkgPath, ".git") {
   229  		pkgPath = pkgPath[:len(pkgPath)-len(".git")]
   230  	}
   231  
   232  	return pkgPath, url, nil
   233  }