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