go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/update.go (about) 1 // Copyright 2016 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package jiri 6 7 import ( 8 "archive/zip" 9 "bufio" 10 "bytes" 11 "encoding/json" 12 "fmt" 13 "io" 14 "net/http" 15 "net/url" 16 "os" 17 "path" 18 "path/filepath" 19 "runtime" 20 "strings" 21 "syscall" 22 23 "go.fuchsia.dev/jiri/osutil" 24 "go.fuchsia.dev/jiri/version" 25 ) 26 27 const ( 28 JiriRepository = "https://fuchsia.googlesource.com/jiri" 29 JiriCIPDEndPoint = "https://chrome-infra-packages.appspot.com/dl/fuchsia/tools/jiri" 30 ) 31 32 var ( 33 updateTestVersionErr = fmt.Errorf("jiri has test version") 34 updateVersionErr = fmt.Errorf("jiri is already at latest version") 35 updateNotAvailableErr = fmt.Errorf("latest version of jiri not available") 36 ) 37 38 // Update checks whether a new version of Jiri is available and if so, 39 // it will download it and replace the current version with the new one. 40 func Update(force bool) error { 41 if !force && version.GitCommit == "" { 42 return updateTestVersionErr 43 } 44 commit, err := getCurrentCommit(JiriRepository) 45 if err != nil { 46 return err 47 } 48 if force || commit != version.GitCommit { 49 // CIPD HTTP endpoint does not allow HTTP HEAD. 50 // Download the Jiri archive directly. 51 b, err := downloadBinary(JiriCIPDEndPoint, commit) 52 if err == updateNotAvailableErr { 53 return err 54 } 55 if err != nil { 56 return fmt.Errorf("cannot download latest jiri binary, %s", err) 57 } 58 unarchivedBinary, err := unarchiveJiri(b) 59 if err != nil { 60 return err 61 } 62 path, err := osutil.Executable() 63 if err != nil { 64 return fmt.Errorf("cannot get executable path, %s", err) 65 } 66 return updateExecutable(path, unarchivedBinary) 67 } 68 return updateVersionErr 69 } 70 71 func unarchiveJiri(b []byte) ([]byte, error) { 72 zipReader, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) 73 if err != nil { 74 return nil, fmt.Errorf("Failed to read jiri archive: %v", err) 75 } 76 for _, file := range zipReader.File { 77 if file.Name == "jiri" { 78 fileReader, err := file.Open() 79 defer fileReader.Close() 80 if err != nil { 81 return nil, fmt.Errorf("Failed to read jiri archive: %v", err) 82 } 83 return io.ReadAll(fileReader) 84 } 85 } 86 return nil, fmt.Errorf("Cannot find jiri in update archive") 87 } 88 89 func UpdateAndExecute(force bool) error { 90 // Capture executable path before it is replaced in Update func 91 path, err := osutil.Executable() 92 if err != nil { 93 return fmt.Errorf("cannot get executable path, %s", err) 94 } 95 if err := Update(force); err != nil { 96 if err != updateNotAvailableErr && err != updateVersionErr && 97 err != updateTestVersionErr { 98 return err 99 } 100 return nil 101 } 102 103 args := []string{} 104 for _, a := range os.Args { 105 if !strings.HasPrefix(a, "-force-autoupdate") { 106 args = append(args, a) 107 } 108 } 109 110 // Run the update version. 111 if err = syscall.Exec(path, args, os.Environ()); err != nil { 112 return fmt.Errorf("cannot execute %s: %s", path, err) 113 } 114 return nil 115 } 116 117 func getCurrentCommit(repository string) (string, error) { 118 u, err := url.Parse(repository) 119 if err != nil { 120 return "", err 121 } 122 if u.Scheme != "http" && u.Scheme != "https" { 123 return "", fmt.Errorf("remote host scheme is not http(s): %s", repository) 124 } 125 u.Path = path.Join(u.Path, "+refs/heads/main") 126 q := u.Query() 127 q.Set("format", "json") 128 u.RawQuery = q.Encode() 129 130 // Use Gitiles to find out the latest revision. 131 req, err := http.NewRequest("GET", u.String(), nil) 132 if err != nil { 133 return "", err 134 } 135 req.Header.Add("Accept", "application/json") 136 res, err := http.DefaultClient.Do(req) 137 if err != nil { 138 return "", err 139 } 140 defer res.Body.Close() 141 if res.StatusCode != http.StatusOK { 142 return "", fmt.Errorf("HTTP request failed: %v", http.StatusText(res.StatusCode)) 143 } 144 145 r := bufio.NewReader(res.Body) 146 147 // The first line of the input is the XSSI guard ")]}'". 148 if _, err := r.ReadSlice('\n'); err != nil { 149 return "", err 150 } 151 152 var result map[string]struct { 153 Value string `json:"value"` 154 } 155 if err := json.NewDecoder(r).Decode(&result); err != nil { 156 return "", err 157 } 158 if v, ok := result["refs/heads/main"]; ok { 159 return v.Value, nil 160 } else { 161 return "", fmt.Errorf("cannot find current commit") 162 } 163 } 164 165 func downloadBinary(endpoint, version string) ([]byte, error) { 166 os := runtime.GOOS 167 if os == "darwin" { 168 os = "mac" 169 } 170 url := fmt.Sprintf("%s/%s-%s/+/git_revision:%s", endpoint, os, runtime.GOARCH, version) 171 res, err := http.Get(url) 172 if err != nil { 173 return nil, err 174 } 175 176 if res.StatusCode == http.StatusNotFound { 177 return nil, updateNotAvailableErr 178 } 179 if res.StatusCode != http.StatusOK { 180 return nil, fmt.Errorf("HTTP request failed: %v", http.StatusText(res.StatusCode)) 181 } 182 defer res.Body.Close() 183 184 bytes, err := io.ReadAll(res.Body) 185 if err != nil { 186 return nil, err 187 } 188 189 return bytes, nil 190 } 191 192 func updateExecutable(path string, b []byte) error { 193 fi, err := os.Stat(path) 194 if err != nil { 195 return err 196 } 197 198 dir := filepath.Dir(path) 199 200 // Write the new version to a file. 201 newfile, err := os.CreateTemp(dir, "jiri") 202 if err != nil { 203 return err 204 } 205 206 if _, err := newfile.Write(b); err != nil { 207 return err 208 } 209 210 if err := newfile.Chmod(fi.Mode()); err != nil { 211 return err 212 } 213 214 if err := newfile.Close(); err != nil { 215 return err 216 } 217 218 // Backup the existing version. 219 oldfile, err := os.CreateTemp(dir, "jiri") 220 if err != nil { 221 return err 222 } 223 defer os.Remove(oldfile.Name()) 224 225 if err := oldfile.Close(); err != nil { 226 return err 227 } 228 229 err = osutil.Rename(path, oldfile.Name()) 230 if err != nil { 231 return err 232 } 233 234 // Replace the existing version. 235 err = osutil.Rename(newfile.Name(), path) 236 if err != nil { 237 // Try to rollback the change in case of error. 238 rerr := osutil.Rename(oldfile.Name(), path) 239 if rerr != nil { 240 return rerr 241 } 242 return err 243 } 244 245 return nil 246 }