github.com/jdhenke/godel@v0.0.0-20161213181855-abeb3861bf0d/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  	"os"
    20  	"os/exec"
    21  	"path"
    22  	"path/filepath"
    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, info bool) error {
    33  	gopath := os.Getenv("GOPATH")
    34  	if gopath == "" {
    35  		return fmt.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  			if info {
    47  				fmt.Printf("GOROOT environment variable is not set and the value provided in the compiled code does not exist locally.\n")
    48  			}
    49  
    50  			if goEnvRoot != "" {
    51  				shellCfgFiles := getShellCfgFiles()
    52  				if info {
    53  					fmt.Printf("'go env' reports that GOROOT is %v. Suggested fix:\n\texport GOROOT=%v\n", goEnvRoot, goEnvRoot)
    54  					for _, currCfgFile := range shellCfgFiles {
    55  						fmt.Printf("\techo \"export GOROOT=%v\" >> %q \n", goEnvRoot, currCfgFile)
    56  					}
    57  				} else {
    58  					for _, currCfgFile := range shellCfgFiles {
    59  						fmt.Printf("Adding \"export GOROOT=%v\" to %v...\n", goEnvRoot, currCfgFile)
    60  						if err := appendToFile(currCfgFile, fmt.Sprintf("export GOROOT=%v\n", goEnvRoot)); err != nil {
    61  							fmt.Printf("Failed to add \"export GOROOT=%v\" in %v\n", goEnvRoot, currCfgFile)
    62  						}
    63  					}
    64  				}
    65  			} else {
    66  				fmt.Printf("Unable to determine GOROOT using 'go env GOROOT'. Ensure that Go was installed properly with source files.\n")
    67  			}
    68  		}
    69  	}
    70  
    71  	srcPath, err := gitRepoRootPath(wd)
    72  	if err != nil {
    73  		return fmt.Errorf("Directory %q must be in a git project", wd)
    74  	}
    75  
    76  	pkgPath, gitRemoteURL, err := gitRemoteOriginPkgPath(wd)
    77  	if err != nil {
    78  		return fmt.Errorf("Unable to determine URL of git remote for %v: verify that the project was checked out from a git remote and that the remote URL is set", wd)
    79  	} else if pkgPath == "" {
    80  		return fmt.Errorf("Unable to determine the expected package path from git remote URL of %v", gitRemoteURL)
    81  	}
    82  
    83  	dstPath := path.Join(gopath, "src", pkgPath)
    84  	if srcPath == dstPath {
    85  		fmt.Printf("Project appears to be in the correct location\n")
    86  		return nil
    87  	}
    88  	dstPathParentDir := path.Dir(dstPath)
    89  
    90  	fmt.Printf("Project path %q differs from expected path %q\n", srcPath, dstPath)
    91  	if _, err := os.Stat(dstPath); !os.IsNotExist(err) {
    92  		if !info {
    93  			return fmt.Errorf("Destination path %q already exists", dstPath)
    94  		}
    95  		fmt.Printf("Expected destination path %q already exists.\nIf 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.\n", dstPath)
    96  		return nil
    97  	}
    98  
    99  	var fixOperation func(srcPath, dstPath string) error
   100  	var fixOpMessage string
   101  	if pathsOnSameDevice(srcPath, gopath) {
   102  		if info {
   103  			fmt.Printf("Project and GOPATH are on same device. Suggested fix:\n\tmkdir -p %q && mv %q %q\n", dstPathParentDir, srcPath, dstPath)
   104  		}
   105  		fixOpMessage = fmt.Sprintf("Moving %q to %q...\n", wd, dstPath)
   106  		fixOperation = os.Rename
   107  	} else {
   108  		if info {
   109  			fmt.Printf("Project and GOPATH are on different devices. Suggested fix:\n\tmkdir -p %q && cp -r %q %q\n", dstPathParentDir, srcPath, dstPath)
   110  		}
   111  		fixOpMessage = fmt.Sprintf("Copying %q to %q...\n", wd, dstPath)
   112  		fixOperation = func(srcPath, dstPath string) error {
   113  			return shutil.CopyTree(srcPath, dstPath, nil)
   114  		}
   115  	}
   116  
   117  	if info {
   118  		fmt.Printf("\n%v found issues. Run the following godel command to implement suggested fixes:\n\t%v\n", cmd, cmd)
   119  	} else {
   120  		if err := os.MkdirAll(dstPathParentDir, 0755); err != nil {
   121  			return fmt.Errorf("Failed to create path to %q: %v", dstPathParentDir, err)
   122  		}
   123  		fmt.Printf(fixOpMessage)
   124  		if err := fixOperation(srcPath, dstPath); err != nil {
   125  			return err
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  func VerifyGoEnv(wd string) {
   132  	goroot := os.Getenv("GOROOT")
   133  	if goroot == "" {
   134  		// GOROOT is not set as environment variable: check value of GOROOT provided by compiled code
   135  		compiledGoRoot := runtime.GOROOT()
   136  		if info, err := os.Stat(compiledGoRoot); err != nil || !info.IsDir() {
   137  			// compiled GOROOT does not exist: get GOROOT from "go env GOROOT" command and display warning
   138  			goEnvRoot := goEnvRoot()
   139  
   140  			title := "GOROOT environment variable is empty"
   141  			content := fmt.Sprintf("GOROOT is required to build Go code. The GOROOT environment variable was not set and the value of GOROOT in the compiled code does not exist locally.\n")
   142  			content += fmt.Sprintf("Run the godel command '%v --info' for more information.", cmd)
   143  
   144  			if goEnvRoot != "" {
   145  				content += fmt.Sprintf("\nFalling back to value provided by 'go env GOROOT': %v", goEnvRoot)
   146  				if err := os.Setenv("GOROOT", goEnvRoot); err != nil {
   147  					fmt.Printf("Failed to set GOROOT environment variable: %v", err)
   148  					return
   149  				}
   150  			}
   151  			printWarning(title, content)
   152  		}
   153  	}
   154  
   155  	gopath := os.Getenv("GOPATH")
   156  	if gopath == "" {
   157  		title := "GOPATH environment variable is empty"
   158  		content := fmt.Sprintf("GOPATH is required to build Go code.\nSee https://golang.org/doc/code.html#GOPATH for more information.")
   159  		printWarning(title, content)
   160  		return
   161  	}
   162  
   163  	goSrcPath := path.Join(gopath, "src")
   164  	if !isSubdir(goSrcPath, wd) {
   165  		title := "project directory is not a subdirectory of $GOPATH/src"
   166  		content := fmt.Sprintf("%q is not a subdirectory of the GOPATH/src directory (%q): the project location is likely incorrect.\n", wd, goSrcPath)
   167  		content += fmt.Sprintf("Run the godel command '%v --info' for more information.", cmd)
   168  		printWarning(title, content)
   169  	}
   170  }
   171  
   172  func goEnvRoot() string {
   173  	if output, err := exec.Command("go", "env", "GOROOT").CombinedOutput(); err == nil {
   174  		return strings.TrimSpace(string(output))
   175  	}
   176  	return ""
   177  }
   178  
   179  func getShellCfgFiles() []string {
   180  	var files []string
   181  	for _, currCfg := range []string{".bash_profile", ".zshrc"} {
   182  		if currFile := checkForCfgFile(currCfg); currFile != "" {
   183  			files = append(files, currFile)
   184  		}
   185  	}
   186  	return files
   187  }
   188  
   189  func appendToFile(filename, content string) (rErr error) {
   190  	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0666)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	defer func() {
   195  		if err := f.Close(); err != nil && rErr == nil {
   196  			rErr = errors.Wrapf(err, "failed to close file %s in defer", filename)
   197  		}
   198  	}()
   199  	if _, err = f.WriteString(content); err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  func checkForCfgFile(name string) string {
   206  	cfgFile := path.Join(os.Getenv("HOME"), name)
   207  	if _, err := os.Stat(cfgFile); err == nil {
   208  		return cfgFile
   209  	}
   210  	return ""
   211  }
   212  
   213  func printWarning(title, content string) {
   214  	warning := fmt.Sprintf("| WARNING: %v |", title)
   215  	fmt.Println(strings.Repeat("-", len(warning)))
   216  	fmt.Println(warning)
   217  	fmt.Println(strings.Repeat("-", len(warning)))
   218  	fmt.Println(content)
   219  	fmt.Println()
   220  }
   221  
   222  func pathsOnSameDevice(p1, p2 string) bool {
   223  	id1, ok := getDeviceID(p1)
   224  	if !ok {
   225  		return false
   226  	}
   227  	id2, ok := getDeviceID(p2)
   228  	if !ok {
   229  		return false
   230  	}
   231  	return id1 == id2
   232  }
   233  
   234  func getDeviceID(p string) (interface{}, bool) {
   235  	fi, err := os.Stat(p)
   236  	if err != nil {
   237  		return 0, false
   238  	}
   239  	s := fi.Sys()
   240  	switch s := s.(type) {
   241  	case *syscall.Stat_t:
   242  		return s.Dev, true
   243  	}
   244  	return 0, false
   245  }
   246  
   247  func isSubdir(base, dst string) bool {
   248  	relPath, err := filepath.Rel(base, dst)
   249  	return err == nil && !strings.HasPrefix(relPath, "..")
   250  }
   251  
   252  func gitRepoRootPath(dir string) (string, error) {
   253  	cmd := exec.Command("git", "rev-parse", "--show-toplevel")
   254  	cmd.Dir = dir
   255  	output, err := cmd.CombinedOutput()
   256  	if err != nil {
   257  		return "", fmt.Errorf("%v failed: could not determine root of git repository for directory %v", cmd.Args, dir)
   258  	}
   259  	return strings.TrimSpace(string(output)), nil
   260  }
   261  
   262  func gitRemoteOriginPkgPath(dir string) (string, string, error) {
   263  	cmd := exec.Command("git", "ls-remote", "--get-url")
   264  	cmd.Dir = dir
   265  	bytes, err := cmd.CombinedOutput()
   266  	if err != nil {
   267  		return "", "", fmt.Errorf("%v failed: git remote URL is not set", cmd.Args)
   268  	}
   269  
   270  	pkgPath := ""
   271  	url := strings.TrimSpace(string(bytes))
   272  	if protocolSeparator := regexp.MustCompile("https?://").FindStringIndex(url); protocolSeparator != nil {
   273  		if protocolSeparator[0] == 0 {
   274  			pkgPath = url[protocolSeparator[1]:]
   275  		}
   276  	} else if atSign := strings.Index(url, "@"); atSign != -1 {
   277  		// assume SSH format of "user@host:org/repo"
   278  		pkgPath = url[atSign+1:]
   279  		pkgPath = strings.Replace(pkgPath, ":", "/", 1)
   280  	}
   281  
   282  	// trim ".git" suffix if present
   283  	if strings.HasSuffix(pkgPath, ".git") {
   284  		pkgPath = pkgPath[:len(pkgPath)-len(".git")]
   285  	}
   286  
   287  	return pkgPath, url, nil
   288  }