github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"github.com/juju/version"
    19  
    20  	"github.com/juju/juju/juju/names"
    21  )
    22  
    23  // Archive writes the executable files found in the given directory in
    24  // gzipped tar format to w.
    25  func Archive(w io.Writer, dir string) error {
    26  	entries, err := ioutil.ReadDir(dir)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	gzw := gzip.NewWriter(w)
    32  	defer closeErrorCheck(&err, gzw)
    33  
    34  	tarw := tar.NewWriter(gzw)
    35  	defer closeErrorCheck(&err, tarw)
    36  
    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  	}
    55  	return nil
    56  }
    57  
    58  // archiveAndSHA256 calls Archive with the provided arguments,
    59  // and returns a hex-encoded SHA256 hash of the resulting
    60  // archive.
    61  func archiveAndSHA256(w io.Writer, dir string) (sha256hash string, err error) {
    62  	h := sha256.New()
    63  	if err := Archive(io.MultiWriter(h, w), dir); err != nil {
    64  		return "", err
    65  	}
    66  	return fmt.Sprintf("%x", h.Sum(nil)), err
    67  }
    68  
    69  // copyFile writes the contents of the given file to w.
    70  func copyFile(w io.Writer, file string) error {
    71  	f, err := os.Open(file)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer f.Close()
    76  	_, err = io.Copy(w, f)
    77  	return err
    78  }
    79  
    80  // tarHeader returns a tar file header given the file's stat
    81  // information.
    82  func tarHeader(i os.FileInfo) *tar.Header {
    83  	return &tar.Header{
    84  		Typeflag:   tar.TypeReg,
    85  		Name:       i.Name(),
    86  		Size:       i.Size(),
    87  		Mode:       int64(i.Mode() & 0777),
    88  		ModTime:    i.ModTime(),
    89  		AccessTime: i.ModTime(),
    90  		ChangeTime: i.ModTime(),
    91  		Uname:      "ubuntu",
    92  		Gname:      "ubuntu",
    93  	}
    94  }
    95  
    96  // isExecutable returns whether the given info
    97  // represents a regular file executable by (at least) the user.
    98  func isExecutable(i os.FileInfo) bool {
    99  	return i.Mode()&(0100|os.ModeType) == 0100
   100  }
   101  
   102  // closeErrorCheck means that we can ensure that
   103  // Close errors do not get lost even when we defer them,
   104  func closeErrorCheck(errp *error, c io.Closer) {
   105  	err := c.Close()
   106  	if *errp == nil {
   107  		*errp = err
   108  	}
   109  }
   110  
   111  func setenv(env []string, val string) []string {
   112  	prefix := val[0 : strings.Index(val, "=")+1]
   113  	for i, eval := range env {
   114  		if strings.HasPrefix(eval, prefix) {
   115  			env[i] = val
   116  			return env
   117  		}
   118  	}
   119  	return append(env, val)
   120  }
   121  
   122  func findExecutable(execFile string) (string, error) {
   123  	logger.Debugf("looking for: %s", execFile)
   124  	if filepath.IsAbs(execFile) {
   125  		return execFile, nil
   126  	}
   127  
   128  	dir, file := filepath.Split(execFile)
   129  
   130  	// Now we have two possibilities:
   131  	//   file == path indicating that the PATH was searched
   132  	//   dir != "" indicating that it is a relative path
   133  
   134  	if dir == "" {
   135  		path := os.Getenv("PATH")
   136  		for _, name := range filepath.SplitList(path) {
   137  			result := filepath.Join(name, file)
   138  			// Use exec.LookPath() to check if the file exists and is executable`
   139  			f, err := exec.LookPath(result)
   140  			if err == nil {
   141  				return f, nil
   142  			}
   143  		}
   144  
   145  		return "", fmt.Errorf("could not find %q in the path", file)
   146  	}
   147  	cwd, err := os.Getwd()
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  	return filepath.Clean(filepath.Join(cwd, execFile)), nil
   152  }
   153  
   154  func copyExistingJujud(dir string) error {
   155  	// Assume that the user is running juju.
   156  	jujuLocation, err := findExecutable(os.Args[0])
   157  	if err != nil {
   158  		logger.Infof("%v", err)
   159  		return err
   160  	}
   161  	jujudLocation := filepath.Join(filepath.Dir(jujuLocation), names.Jujud)
   162  	logger.Debugf("checking: %s", jujudLocation)
   163  	info, err := os.Stat(jujudLocation)
   164  	if err != nil {
   165  		logger.Infof("couldn't find existing jujud")
   166  		return err
   167  	}
   168  	logger.Infof("found existing jujud")
   169  	// TODO(thumper): break this out into a util function.
   170  	// copy the file into the dir.
   171  	source, err := os.Open(jujudLocation)
   172  	if err != nil {
   173  		logger.Infof("open source failed: %v", err)
   174  		return err
   175  	}
   176  	defer source.Close()
   177  	target := filepath.Join(dir, names.Jujud)
   178  	logger.Infof("target: %v", target)
   179  	destination, err := os.OpenFile(target, os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode())
   180  	if err != nil {
   181  		logger.Infof("open destination failed: %v", err)
   182  		return err
   183  	}
   184  	defer destination.Close()
   185  	_, err = io.Copy(destination, source)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	return nil
   190  }
   191  
   192  func buildJujud(dir string) error {
   193  	logger.Infof("building jujud")
   194  	cmds := [][]string{
   195  		{"go", "build", "-gccgoflags=-static-libgo", "-o", filepath.Join(dir, names.Jujud), "github.com/juju/juju/cmd/jujud"},
   196  	}
   197  	for _, args := range cmds {
   198  		cmd := exec.Command(args[0], args[1:]...)
   199  		out, err := cmd.CombinedOutput()
   200  		if err != nil {
   201  			return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
   202  		}
   203  	}
   204  	return nil
   205  }
   206  
   207  // BundleToolsFunc is a function which can bundle all the current juju tools
   208  // in gzipped tar format to the given writer.
   209  type BundleToolsFunc func(w io.Writer, forceVersion *version.Number) (version.Binary, string, error)
   210  
   211  // Override for testing.
   212  var BundleTools BundleToolsFunc = bundleTools
   213  
   214  // BundleTools bundles all the current juju tools in gzipped tar
   215  // format to the given writer.
   216  // If forceVersion is not nil, a FORCE-VERSION file is included in
   217  // the tools bundle so it will lie about its current version number.
   218  func bundleTools(w io.Writer, forceVersion *version.Number) (tvers version.Binary, sha256Hash string, err error) {
   219  	dir, err := ioutil.TempDir("", "juju-tools")
   220  	if err != nil {
   221  		return version.Binary{}, "", err
   222  	}
   223  	defer os.RemoveAll(dir)
   224  
   225  	if err := copyExistingJujud(dir); err != nil {
   226  		logger.Debugf("copy existing failed: %v", err)
   227  		if err := buildJujud(dir); err != nil {
   228  			return version.Binary{}, "", err
   229  		}
   230  	}
   231  
   232  	if forceVersion != nil {
   233  		logger.Debugf("forcing version to %s", forceVersion)
   234  		if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
   235  			return version.Binary{}, "", err
   236  		}
   237  	}
   238  	cmd := exec.Command(filepath.Join(dir, names.Jujud), "version")
   239  	out, err := cmd.CombinedOutput()
   240  	if err != nil {
   241  		return version.Binary{}, "", fmt.Errorf("cannot get version from %q: %v; %s", cmd.Args[0], err, out)
   242  	}
   243  	tvs := strings.TrimSpace(string(out))
   244  	tvers, err = version.ParseBinary(tvs)
   245  	if err != nil {
   246  		return version.Binary{}, "", fmt.Errorf("invalid version %q printed by jujud", tvs)
   247  	}
   248  
   249  	sha256hash, err := archiveAndSHA256(w, dir)
   250  	if err != nil {
   251  		return version.Binary{}, "", err
   252  	}
   253  	return tvers, sha256hash, err
   254  }