launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/tools/build.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package tools
     5  
     6  import (
     7  	"archive/tar"
     8  	"compress/gzip"
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"launchpad.net/juju-core/version"
    19  )
    20  
    21  // archive writes the executable files found in the given directory in
    22  // gzipped tar format to w, returning the SHA256 hash of the resulting file.
    23  // An error is returned if an entry inside dir is not a regular executable file.
    24  func archive(w io.Writer, dir string) (string, error) {
    25  	entries, err := ioutil.ReadDir(dir)
    26  	if err != nil {
    27  		return "", err
    28  	}
    29  
    30  	gzw := gzip.NewWriter(w)
    31  	defer closeErrorCheck(&err, gzw)
    32  
    33  	tarw := tar.NewWriter(gzw)
    34  	defer closeErrorCheck(&err, tarw)
    35  
    36  	sha256hash := sha256.New()
    37  	for _, ent := range entries {
    38  		h := tarHeader(ent)
    39  		logger.Debugf("adding entry: %#v", h)
    40  		// ignore local umask
    41  		if isExecutable(ent) {
    42  			h.Mode = 0755
    43  		} else {
    44  			h.Mode = 0644
    45  		}
    46  		err := tarw.WriteHeader(h)
    47  		if err != nil {
    48  			return "", err
    49  		}
    50  		fileName := filepath.Join(dir, ent.Name())
    51  		if err := copyFile(tarw, fileName); err != nil {
    52  			return "", err
    53  		}
    54  		if err := copyFile(sha256hash, fileName); err != nil {
    55  			return "", err
    56  		}
    57  	}
    58  	return fmt.Sprintf("%x", sha256hash.Sum(nil)), nil
    59  }
    60  
    61  // copyFile writes the contents of the given file to w.
    62  func copyFile(w io.Writer, file string) error {
    63  	f, err := os.Open(file)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	defer f.Close()
    68  	_, err = io.Copy(w, f)
    69  	return err
    70  }
    71  
    72  // tarHeader returns a tar file header given the file's stat
    73  // information.
    74  func tarHeader(i os.FileInfo) *tar.Header {
    75  	return &tar.Header{
    76  		Typeflag:   tar.TypeReg,
    77  		Name:       i.Name(),
    78  		Size:       i.Size(),
    79  		Mode:       int64(i.Mode() & 0777),
    80  		ModTime:    i.ModTime(),
    81  		AccessTime: i.ModTime(),
    82  		ChangeTime: i.ModTime(),
    83  		Uname:      "ubuntu",
    84  		Gname:      "ubuntu",
    85  	}
    86  }
    87  
    88  // isExecutable returns whether the given info
    89  // represents a regular file executable by (at least) the user.
    90  func isExecutable(i os.FileInfo) bool {
    91  	return i.Mode()&(0100|os.ModeType) == 0100
    92  }
    93  
    94  // closeErrorCheck means that we can ensure that
    95  // Close errors do not get lost even when we defer them,
    96  func closeErrorCheck(errp *error, c io.Closer) {
    97  	err := c.Close()
    98  	if *errp == nil {
    99  		*errp = err
   100  	}
   101  }
   102  
   103  func setenv(env []string, val string) []string {
   104  	prefix := val[0 : strings.Index(val, "=")+1]
   105  	for i, eval := range env {
   106  		if strings.HasPrefix(eval, prefix) {
   107  			env[i] = val
   108  			return env
   109  		}
   110  	}
   111  	return append(env, val)
   112  }
   113  
   114  func findExecutable(execFile string) (string, error) {
   115  	logger.Debugf("looking for: %s", execFile)
   116  	if filepath.IsAbs(execFile) {
   117  		return execFile, nil
   118  	}
   119  
   120  	dir, file := filepath.Split(execFile)
   121  
   122  	// Now we have two possibilities:
   123  	//   file == path indicating that the PATH was searched
   124  	//   dir != "" indicating that it is a relative path
   125  
   126  	if dir == "" {
   127  		path := os.Getenv("PATH")
   128  		for _, name := range filepath.SplitList(path) {
   129  			result := filepath.Join(name, file)
   130  			info, err := os.Stat(result)
   131  			if err == nil {
   132  				// Sanity check to see if executable.
   133  				if info.Mode()&0111 != 0 {
   134  					return result, nil
   135  				}
   136  			}
   137  		}
   138  
   139  		return "", fmt.Errorf("could not find %q in the path", file)
   140  	}
   141  	cwd, err := os.Getwd()
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return filepath.Clean(filepath.Join(cwd, execFile)), nil
   146  }
   147  
   148  func copyExistingJujud(dir string) error {
   149  	// Assume that the user is running juju.
   150  	jujuLocation, err := findExecutable(os.Args[0])
   151  	if err != nil {
   152  		logger.Infof("%v", err)
   153  		return err
   154  	}
   155  	jujudLocation := filepath.Join(filepath.Dir(jujuLocation), "jujud")
   156  	logger.Debugf("checking: %s", jujudLocation)
   157  	info, err := os.Stat(jujudLocation)
   158  	if err != nil {
   159  		logger.Infof("couldn't find existing jujud")
   160  		return err
   161  	}
   162  	logger.Infof("found existing jujud")
   163  	// TODO(thumper): break this out into a util function.
   164  	// copy the file into the dir.
   165  	source, err := os.Open(jujudLocation)
   166  	if err != nil {
   167  		logger.Infof("open source failed: %v", err)
   168  		return err
   169  	}
   170  	defer source.Close()
   171  	target := filepath.Join(dir, "jujud")
   172  	logger.Infof("target: %v", target)
   173  	destination, err := os.OpenFile(target, os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode())
   174  	if err != nil {
   175  		logger.Infof("open destination failed: %v", err)
   176  		return err
   177  	}
   178  	defer destination.Close()
   179  	_, err = io.Copy(destination, source)
   180  	if err != nil {
   181  		return err
   182  	}
   183  	return nil
   184  }
   185  
   186  func buildJujud(dir string) error {
   187  	logger.Infof("building jujud")
   188  	cmds := [][]string{
   189  		{"go", "install", "launchpad.net/juju-core/cmd/jujud"},
   190  		{"strip", dir + "/jujud"},
   191  	}
   192  	env := setenv(os.Environ(), "GOBIN="+dir)
   193  	for _, args := range cmds {
   194  		cmd := exec.Command(args[0], args[1:]...)
   195  		cmd.Env = env
   196  		out, err := cmd.CombinedOutput()
   197  		if err != nil {
   198  			return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
   199  		}
   200  	}
   201  	return nil
   202  }
   203  
   204  // BundleTools bundles all the current juju tools in gzipped tar
   205  // format to the given writer.
   206  // If forceVersion is not nil, a FORCE-VERSION file is included in
   207  // the tools bundle so it will lie about its current version number.
   208  func BundleTools(w io.Writer, forceVersion *version.Number) (tvers version.Binary, sha256Hash string, err error) {
   209  	dir, err := ioutil.TempDir("", "juju-tools")
   210  	if err != nil {
   211  		return version.Binary{}, "", err
   212  	}
   213  	defer os.RemoveAll(dir)
   214  
   215  	if err := copyExistingJujud(dir); err != nil {
   216  		logger.Debugf("copy existing failed: %v", err)
   217  		if err := buildJujud(dir); err != nil {
   218  			return version.Binary{}, "", err
   219  		}
   220  	}
   221  
   222  	if forceVersion != nil {
   223  		logger.Debugf("forcing version to %s", forceVersion)
   224  		if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
   225  			return version.Binary{}, "", err
   226  		}
   227  	}
   228  	cmd := exec.Command(filepath.Join(dir, "jujud"), "version")
   229  	out, err := cmd.CombinedOutput()
   230  	if err != nil {
   231  		return version.Binary{}, "", fmt.Errorf("cannot get version from %q: %v; %s", cmd.Args[0], err, out)
   232  	}
   233  	tvs := strings.TrimSpace(string(out))
   234  	tvers, err = version.ParseBinary(tvs)
   235  	if err != nil {
   236  		return version.Binary{}, "", fmt.Errorf("invalid version %q printed by jujud", tvs)
   237  	}
   238  	sha256Hash, err = archive(w, dir)
   239  	if err != nil {
   240  		return version.Binary{}, "", err
   241  	}
   242  	return tvers, sha256Hash, err
   243  }