github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/cipd/cipd.go (about)

     1  // Copyright 2018 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 cipd
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"crypto/sha256"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io/ioutil"
    16  	"net/http"
    17  	"os"
    18  	"os/exec"
    19  	"path"
    20  	"path/filepath"
    21  	"regexp"
    22  	"runtime"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/btwiuse/jiri"
    29  	"github.com/btwiuse/jiri/log"
    30  	"github.com/btwiuse/jiri/version"
    31  	"golang.org/x/sync/semaphore"
    32  )
    33  
    34  const (
    35  	cipdBackend = "https://chrome-infra-packages.appspot.com"
    36  	// This git hash corresponds to a commit in https://chromium.googlesource.com/infra/infra
    37  	// to update the pinned version of the CIPD client in the DEPS file.
    38  	cipdVersion       = "git_revision:0c0bef53cf8083d96ea39d24152fd0362b521ed7"
    39  	cipdVersionDigest = `
    40  # This file was generated by
    41  #
    42  #  cipd selfupdate-roll -version-file .cipd_version \
    43  #      -version git_revision:0c0bef53cf8083d96ea39d24152fd0362b521ed7
    44  #
    45  # Do not modify manually. All changes will be overwritten.
    46  # Use 'cipd selfupdate-roll ...' to modify.
    47  
    48  linux-386       sha256  4958b7a7623231c6b1a530da5bc88fb1468e7fa165ddb8802cc14ad842de8212
    49  linux-amd64     sha256  292213783cd4302e26a501085adda272ccbd7d4733a865bbeb3cb050f3b34194
    50  linux-arm64     sha256  759f5a7c1433f6d99c99c4156e3997c3926c2435c960212c6e324ab6bd64ad01
    51  linux-armv6l    sha256  b4bcdfaa45c2d7047ced6594368758dc624bf718babfe14851e6de7a994f24b8
    52  linux-mips64    sha256  62fd72cbe4131d820fda543458007fbeca81a9234c86a3ae6910255ca17a679f
    53  linux-mips64le  sha256  2c8cda73279b00fc46cf55b09b3db984f5a8222753b87e3ed3ee280c807fb942
    54  linux-mipsle    sha256  6d0b68a8f013c7ee1c4f4980a788aaddf3bbc4cfecdd811e6e0df9b481a0d064
    55  linux-ppc64     sha256  d3b6146d37d2aec0514ac67151aed61eb995a1015fee7026fc0cac415d1282ff
    56  linux-ppc64le   sha256  fcb0c726dc2c37308b2ece48a095027dcbfb86e65bc677a8b1b3fd18d20dad23
    57  linux-s390x     sha256  2f4afbea4b1f5d7c4c474882de661bd35e6fdb4ecd34533a05ead8b48a3602ea
    58  mac-amd64       sha256  7a8105e4e2bc95f2cc3060dcffb44c53391ff7e380f89be276102f619910ae17
    59  windows-386     sha256  59c6d695a24973ef42e731e86fb055827df4f3e060481605e36c6e2d8dfd6ff0
    60  windows-amd64   sha256  1d26180a78ac11c5a940e7f7a26cdf483cf81ccf0e98e29007932a2eb7d621e0
    61  `
    62  	cipdNotLoggedInStr = "Not logged in"
    63  )
    64  
    65  var (
    66  	// CipdPlatform represents the current runtime platform in cipd platform notation.
    67  	CipdPlatform   Platform
    68  	cipdOS         string
    69  	cipdArch       string
    70  	cipdBinary     string
    71  	selfUpdateOnce sync.Once
    72  	templateRE     = regexp.MustCompile(`\${[^}]*}`)
    73  
    74  	// ErrSkipTemplate may be returned from Expander.Expand to indicate that
    75  	// a given expansion doesn't apply to the current template parameters. For
    76  	// example, expanding `"foo/${os=linux,mac}"` with a template parameter of "os"
    77  	// == "win", would return ErrSkipTemplate.
    78  	ErrSkipTemplate = errors.New("package template does not apply to the current system")
    79  )
    80  
    81  func init() {
    82  	cipdOS = runtime.GOOS
    83  	cipdArch = runtime.GOARCH
    84  	if cipdOS == "darwin" {
    85  		cipdOS = "mac"
    86  	}
    87  	if cipdArch == "arm" {
    88  		cipdArch = "armv6l"
    89  	}
    90  	CipdPlatform = Platform{cipdOS, cipdArch}
    91  }
    92  
    93  func fetchBinary(binaryPath, platform, version, digest string) error {
    94  	cipdURL := fmt.Sprintf("%s/client?platform=%s&version=%s", cipdBackend, platform, version)
    95  	data, err := fetchFile(cipdURL)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	if verified, err := verifyDigest(data, digest); err != nil || !verified {
   100  		if err != nil {
   101  			return err
   102  		}
   103  		return errors.New("cipd failed integrity test")
   104  	}
   105  	// cipd binary verified. Save to disk
   106  	if _, err := os.Stat(filepath.Dir(binaryPath)); os.IsNotExist(err) {
   107  		if err := os.MkdirAll(filepath.Dir(binaryPath), 0755); err != nil {
   108  			return fmt.Errorf("failed to create parent directory %q for cipd: %v", filepath.Dir(binaryPath), err)
   109  		}
   110  	}
   111  	return writeFile(binaryPath, data)
   112  }
   113  
   114  // Bootstrap returns the path of a valid cipd binary. It will fetch cipd from
   115  // remote if a valid cipd binary is not found. It will update cipd if there
   116  // is a new version.
   117  func Bootstrap(binaryPath string) (string, error) {
   118  	cipdBinary = binaryPath
   119  	bootstrap := func() error {
   120  		// Fetch cipd digest
   121  		cipdDigest, _, err := fetchDigest(CipdPlatform.String())
   122  		if err != nil {
   123  			return err
   124  		}
   125  		if cipdBinary == "" {
   126  			return errors.New("cipd binary path was not set")
   127  		}
   128  		if err != nil {
   129  			return err
   130  		}
   131  		return fetchBinary(cipdBinary, CipdPlatform.String(), cipdVersion, cipdDigest)
   132  	}
   133  
   134  	getCipd := func() (string, error) {
   135  		if cipdBinary == "" {
   136  			return "", errors.New("cipd binary path was not set")
   137  		}
   138  		fileInfo, err := os.Stat(cipdBinary)
   139  		if err != nil {
   140  			if os.IsNotExist(err) {
   141  				return "", fmt.Errorf("cipd binary was not found at %q", cipdBinary)
   142  			}
   143  			return "", err
   144  		}
   145  		// Check if cipd binary has execution permission
   146  		if fileInfo.Mode()&0111 == 0 {
   147  			return "", fmt.Errorf("cipd binary at %q is not executable", cipdBinary)
   148  		}
   149  		return cipdBinary, nil
   150  	}
   151  
   152  	cipdPath, err := getCipd()
   153  	if err != nil {
   154  		// Could not find cipd binary or cipd is invalid
   155  		// Bootstrap it from scratch
   156  		if err := bootstrap(); err != nil {
   157  			return "", err
   158  		}
   159  		return cipdBinary, nil
   160  	}
   161  	// cipd is found, do self update
   162  	var e error
   163  	selfUpdateOnce.Do(func() {
   164  		e = selfUpdate(cipdPath, cipdVersion)
   165  	})
   166  	if e != nil {
   167  		// Self update is unsuccessful, redo bootstrap
   168  		if err := bootstrap(); err != nil {
   169  			return "", err
   170  		}
   171  	}
   172  	return cipdPath, nil
   173  }
   174  
   175  // FuchsiaPlatform returns a Platform struct which can be used in
   176  // determing the correct path for prebuilt packages. It replace
   177  // the os and arch names from cipd format to a format used by
   178  // Fuchsia developers.
   179  func FuchsiaPlatform(plat Platform) Platform {
   180  	retPlat := Platform{
   181  		OS:   plat.OS,
   182  		Arch: plat.Arch,
   183  	}
   184  	// Currently cipd use "amd64" for x86_64 while fuchsia use "x64",
   185  	// replace "amd64" with "x64".
   186  	// There might be other differences that need to be addressed in
   187  	// the future.
   188  	switch retPlat.Arch {
   189  	case "amd64":
   190  		retPlat.Arch = "x64"
   191  	}
   192  	return retPlat
   193  }
   194  
   195  func fetchDigest(platform string) (digest, method string, err error) {
   196  	var digestBuf bytes.Buffer
   197  	digestBuf.Write([]byte(cipdVersionDigest))
   198  	digestScanner := bufio.NewScanner(&digestBuf)
   199  	for digestScanner.Scan() {
   200  		curLine := digestScanner.Text()
   201  		if len(curLine) == 0 || curLine[0] == '#' {
   202  			// Skip comment or empty line
   203  			continue
   204  		}
   205  		fields := strings.Fields(curLine)
   206  		if len(fields) != 3 {
   207  			return "", "", errors.New("unsupported cipd digest file format")
   208  		}
   209  		if fields[0] == platform {
   210  			digest = fields[2]
   211  			method = fields[1]
   212  			err = nil
   213  			return
   214  		}
   215  	}
   216  	return "", "", errors.New("no matching platform found in cipd digest file")
   217  }
   218  
   219  func selfUpdate(cipdPath, cipdVersion string) error {
   220  	args := []string{"selfupdate", "-version", cipdVersion, "-service-url", cipdBackend}
   221  	command := exec.Command(cipdPath, args...)
   222  	return command.Run()
   223  }
   224  
   225  func writeFile(filePath string, data []byte) error {
   226  	tempFile, err := ioutil.TempFile(path.Dir(filePath), "cipd.*")
   227  	if err != nil {
   228  		return err
   229  	}
   230  	defer tempFile.Close()
   231  	defer os.Remove(tempFile.Name())
   232  	if _, err := tempFile.Write(data); err != nil {
   233  		// Write errors
   234  		return errors.New("I/O error while downloading cipd binary")
   235  	}
   236  	// Set mode to rwxr-xr-x
   237  	if err := tempFile.Chmod(0755); err != nil {
   238  		// Chmod errors
   239  		return errors.New("I/O error while adding executable permission to cipd binary")
   240  	}
   241  	tempFile.Close()
   242  	if err := os.Rename(tempFile.Name(), filePath); err != nil {
   243  		return err
   244  	}
   245  	return nil
   246  }
   247  
   248  func verifyDigest(data []byte, cipdDigest string) (bool, error) {
   249  	hash := sha256.Sum256(data)
   250  	hashString := fmt.Sprintf("%x", hash)
   251  	if hashString == strings.ToLower(cipdDigest) {
   252  		return true, nil
   253  	}
   254  	return false, nil
   255  }
   256  
   257  func getUserAgent() string {
   258  	ua := "jiri/" + version.GitCommit
   259  	if version.GitCommit == "" {
   260  		ua += "debug"
   261  	}
   262  	return ua
   263  }
   264  
   265  func fetchFile(url string) ([]byte, error) {
   266  	client := &http.Client{}
   267  	req, err := http.NewRequest("GET", url, nil)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	req.Header.Set("User-Agent", getUserAgent())
   272  	resp, err := client.Do(req)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	defer resp.Body.Close()
   277  	return ioutil.ReadAll(resp.Body)
   278  }
   279  
   280  type packageACL struct {
   281  	path   string
   282  	access bool
   283  }
   284  
   285  func checkPackageACL(jirix *jiri.X, cipdPath, jsonDir string, c chan<- packageACL) {
   286  	// cipd should be already bootstrapped before this go routine.
   287  	// Silently return a false just in case if cipd is not found.
   288  	if cipdBinary == "" {
   289  		c <- packageACL{path: cipdPath, access: false}
   290  		return
   291  	}
   292  
   293  	jsonFile, err := ioutil.TempFile(jsonDir, "cipd*.json")
   294  	if err != nil {
   295  		jirix.Logger.Warningf("Error while creating temporary file for cipd")
   296  		c <- packageACL{path: cipdPath, access: false}
   297  		return
   298  	}
   299  	jsonFileName := jsonFile.Name()
   300  	jsonFile.Close()
   301  
   302  	args := []string{"acl-check", "-reader", "-json-output", jsonFileName, cipdPath}
   303  	jirix.Logger.Debugf("Invoke cipd with %v", args)
   304  
   305  	command := exec.Command(cipdBinary, args...)
   306  	var stdoutBuf, stderrBuf bytes.Buffer
   307  	command.Stdout = &stdoutBuf
   308  	command.Stderr = &stderrBuf
   309  	// Return false if cipd cannot be executed or output jsonfile contains false.
   310  	if err := command.Run(); err != nil {
   311  		jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stderrBuf.String())
   312  		c <- packageACL{path: cipdPath, access: false}
   313  		return
   314  	}
   315  
   316  	jsonData, err := ioutil.ReadFile(jsonFileName)
   317  	if err != nil {
   318  		c <- packageACL{path: cipdPath, access: false}
   319  		return
   320  	}
   321  
   322  	var result struct {
   323  		Result bool `json:"result"`
   324  	}
   325  	if err := json.Unmarshal(jsonData, &result); err != nil {
   326  		c <- packageACL{path: cipdPath, access: false}
   327  		return
   328  	}
   329  
   330  	if !result.Result {
   331  		c <- packageACL{path: cipdPath, access: false}
   332  		return
   333  	}
   334  
   335  	// Package can be accessed.
   336  	c <- packageACL{path: cipdPath, access: true}
   337  	return
   338  }
   339  
   340  // CheckPackageACL checks cipd's access to packages in map "pkgs". The package
   341  // names in "pkgs" should have trailing '/' removed before calling this
   342  // function.
   343  func CheckPackageACL(jirix *jiri.X, pkgs map[string]bool) error {
   344  	// Not declared as CheckPackageACL(jirix *jiri.X, pkgs map[*package.Package]bool)
   345  	// due to import cycles. Package jiri/package imports jiri/cipd so here we cannot
   346  	// import jiri/package.
   347  	if _, err := Bootstrap(jirix.CIPDPath()); err != nil {
   348  		return err
   349  	}
   350  
   351  	jsonDir, err := ioutil.TempDir("", "jiri_cipd")
   352  	if err != nil {
   353  		return err
   354  	}
   355  	defer os.RemoveAll(jsonDir)
   356  
   357  	c := make(chan packageACL)
   358  	for key := range pkgs {
   359  		go checkPackageACL(jirix, key, jsonDir, c)
   360  	}
   361  
   362  	for i := 0; i < len(pkgs); i++ {
   363  		acl := <-c
   364  		pkgs[acl.path] = acl.access
   365  	}
   366  	return nil
   367  }
   368  
   369  // CheckLoggedIn checks cipd's user login information. It will return true
   370  // if login information is found or return false if login information is not
   371  // found.
   372  func CheckLoggedIn(jirix *jiri.X) (bool, error) {
   373  	cipdPath, err := Bootstrap(jirix.CIPDPath())
   374  	if err != nil {
   375  		return false, err
   376  	}
   377  	args := []string{"auth-info"}
   378  	command := exec.Command(cipdPath, args...)
   379  	var stdoutBuf, stderrBuf bytes.Buffer
   380  	command.Stdout = &stdoutBuf
   381  	command.Stderr = &stderrBuf
   382  	if err := command.Run(); err != nil {
   383  		stdErrMsg := strings.TrimSpace(stderrBuf.String())
   384  		jirix.Logger.Debugf("Error happend while executing cipd, err: %q, stderr: %q", err, stdErrMsg)
   385  		if _, ok := err.(*exec.ExitError); ok && stdErrMsg == cipdNotLoggedInStr {
   386  			return false, nil
   387  		}
   388  		return false, err
   389  	}
   390  	return true, nil
   391  }
   392  
   393  // Ensure runs cipd binary's ensure functionality over file. Fetched packages will be
   394  // saved to projectRoot directory. Parameter timeout is in minutes.
   395  func Ensure(jirix *jiri.X, file, projectRoot string, timeout uint) error {
   396  	cipdPath, err := Bootstrap(jirix.CIPDPath())
   397  	if err != nil {
   398  		return err
   399  	}
   400  	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Minute)
   401  	defer cancel()
   402  	args := []string{
   403  		"ensure",
   404  		"-ensure-file", file,
   405  		"-root", projectRoot,
   406  		"-max-threads", strconv.Itoa(jirix.CipdMaxThreads),
   407  	}
   408  
   409  	// If jiri is *not* running with -v, use the less verbose cipd "warning"
   410  	// log-level.
   411  	if jirix.Logger.LoggerLevel < log.DebugLevel {
   412  		args = append(args, "-log-level", "warning")
   413  	}
   414  
   415  	task := jirix.Logger.AddTaskMsg("Fetching CIPD packages")
   416  	defer task.Done()
   417  	jirix.Logger.Debugf("Invoke cipd with %v", args)
   418  
   419  	// Construct arguments and invoke cipd for ensure file
   420  	command := exec.CommandContext(ctx, cipdPath, args...)
   421  	// Add User-Agent info for cipd
   422  	command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent())
   423  	command.Stdin = os.Stdin
   424  	command.Stdout = os.Stdout
   425  	command.Stderr = os.Stderr
   426  
   427  	err = command.Run()
   428  	if ctx.Err() == context.DeadlineExceeded {
   429  		err = ctx.Err()
   430  	}
   431  	return err
   432  }
   433  
   434  // TODO: Using PackageLock in project package directly will cause an import
   435  // cycle. Remove this type once we solve the this issue.
   436  
   437  // PackageInstance describes package instance id information generated by cipd
   438  // ensure-file-resolve. It is a copy of PackageLock type in project package.
   439  type PackageInstance struct {
   440  	PackageName string
   441  	VersionTag  string
   442  	InstanceID  string
   443  }
   444  
   445  // Resolve runs cipd binary's ensure-file-resolve functionality over file.
   446  // It returns a slice containing resolved packages and cipd instance ids.
   447  func Resolve(jirix *jiri.X, file string) ([]PackageInstance, error) {
   448  	cipdPath, err := Bootstrap(jirix.CIPDPath())
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	args := []string{"ensure-file-resolve", "-ensure-file", file, "-log-level", "warning"}
   453  	jirix.Logger.Debugf("Invoke cipd with %v", args)
   454  
   455  	command := exec.Command(cipdPath, args...)
   456  	command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent())
   457  	var stdoutBuf, stderrBuf bytes.Buffer
   458  	command.Stdin = os.Stdin
   459  	// Redirect outputs since cipd will print verbose information even
   460  	// if log-level is set to warning
   461  	command.Stdout = &stdoutBuf
   462  	command.Stderr = &stderrBuf
   463  	if err := command.Run(); err != nil {
   464  		jirix.Logger.Errorf("cipd returned error: %v", stderrBuf.String())
   465  		return nil, err
   466  	}
   467  
   468  	// cipd generates the version file in the same directory of the ensure file
   469  	// if no error is returned
   470  	versionFile := file[:len(file)-len(".ensure")] + ".version"
   471  	defer os.Remove(versionFile)
   472  	return parseVersions(versionFile)
   473  }
   474  
   475  func parseVersions(file string) ([]PackageInstance, error) {
   476  	versionReader, err := os.Open(file)
   477  	if err != nil {
   478  		return nil, err
   479  	}
   480  	defer versionReader.Close()
   481  	versionScanner := bufio.NewScanner(versionReader)
   482  	// An example cipd version looks like:
   483  	// ==========================================================
   484  	// # Do not modify manually. All changes will be overwritten.
   485  	// fuchsia/clang/linux-amd64
   486  	// 	git_revision:280fa3c2d2ddb0b5dcb31113c0b1c2259982b7e7
   487  	// 	eRoGS8qgx370QAIRgLDmbhpdPey8ti47B2Z3LMzwcXQC
   488  	//
   489  	// fuchsia/clang/mac-amd64
   490  	// 	git_revision:280fa3c2d2ddb0b5dcb31113c0b1c2259982b7e7
   491  	// 	BQhlnpoWG081CyLzA0zB1vCr8YPdb2DO2jnYe3Lsw4oC
   492  	// ===========================================================
   493  	// Parse version file using DFA
   494  
   495  	const (
   496  		stWaitingPkg = "a package name"
   497  		stWaitingVer = "a package version"
   498  		stWaitingIID = "an instance ID"
   499  		stWaitingNL  = "a new line"
   500  	)
   501  
   502  	state := stWaitingPkg
   503  	pkg := ""
   504  	ver := ""
   505  	iid := ""
   506  	lineNo := 0
   507  	makeError := func(fmtStr string, args ...interface{}) error {
   508  		args = append([]interface{}{lineNo}, args...)
   509  		return fmt.Errorf("failed to parse versions file (line %d): "+fmtStr, args...)
   510  	}
   511  	output := make([]PackageInstance, 0)
   512  	for versionScanner.Scan() {
   513  		lineNo++
   514  		line := strings.TrimSpace(versionScanner.Text())
   515  		// Comments are grammatically insignificant (unlike empty lines), so skip
   516  		// the completely.
   517  		if len(line) > 0 && line[0] == '#' {
   518  			continue
   519  		}
   520  
   521  		switch state {
   522  		case stWaitingPkg:
   523  			if line == "" {
   524  				continue // can have more than one empty line between triples
   525  			}
   526  			pkg = line
   527  			state = stWaitingVer
   528  
   529  		case stWaitingVer:
   530  			if line == "" {
   531  				return nil, makeError("expecting a version name, not a new line")
   532  			}
   533  			ver = line
   534  			state = stWaitingIID
   535  
   536  		case stWaitingIID:
   537  			if line == "" {
   538  				return nil, makeError("expecting an instance ID, not a new line")
   539  			}
   540  			iid = line
   541  			output = append(output, PackageInstance{pkg, ver, iid})
   542  			pkg, ver, iid = "", "", ""
   543  			state = stWaitingNL
   544  
   545  		case stWaitingNL:
   546  			if line == "" {
   547  				state = stWaitingPkg
   548  				continue
   549  			}
   550  			return nil, makeError("expecting an empty line between each version definition triple")
   551  		}
   552  	}
   553  	return output, nil
   554  }
   555  
   556  type packageFloatingRef struct {
   557  	pkg      PackageInstance
   558  	err      error
   559  	floating bool
   560  }
   561  
   562  // CheckFloatingRefs determines if pkgs contains a floating ref which shouldn't
   563  // be used normally.
   564  func CheckFloatingRefs(jirix *jiri.X, pkgs map[PackageInstance]bool, plats map[PackageInstance][]Platform) error {
   565  	if _, err := Bootstrap(jirix.CIPDPath()); err != nil {
   566  		return err
   567  	}
   568  
   569  	jsonDir, err := ioutil.TempDir("", "jiri_cipd")
   570  	if err != nil {
   571  		return err
   572  	}
   573  	defer os.RemoveAll(jsonDir)
   574  
   575  	c := make(chan packageFloatingRef)
   576  	sem := semaphore.NewWeighted(10)
   577  	var errBuf bytes.Buffer
   578  	for k := range pkgs {
   579  		plat, ok := plats[k]
   580  		if !ok {
   581  			return fmt.Errorf("Platforms for package \"%s\" is not found", k.PackageName)
   582  		}
   583  		go checkFloatingRefs(jirix, k, plat, jsonDir, sem, c)
   584  	}
   585  
   586  	for i := 0; i < len(pkgs); i++ {
   587  		floatingRef := <-c
   588  		pkgs[floatingRef.pkg] = floatingRef.floating
   589  		if floatingRef.err != nil {
   590  			errBuf.WriteString(fmt.Sprintf("error happened while checking package %q with version %q: %v\n", floatingRef.pkg.PackageName, floatingRef.pkg.VersionTag, floatingRef.err.Error()))
   591  		}
   592  	}
   593  
   594  	if errBuf.Len() != 0 {
   595  		// Remote trailing '\n'
   596  		errBuf.Truncate(errBuf.Len() - 1)
   597  		return errors.New(errBuf.String())
   598  	}
   599  	return nil
   600  }
   601  
   602  type describeJSON struct {
   603  	Refs []refsJSON `json:"refs,omitempty"`
   604  }
   605  
   606  type refsJSON struct {
   607  	Ref string `json:"ref,omitempty"`
   608  }
   609  
   610  func checkFloatingRefs(jirix *jiri.X, pkg PackageInstance, plats []Platform, jsonDir string, sem *semaphore.Weighted, c chan<- packageFloatingRef) {
   611  	// cipd should already bootstrapped before calling
   612  	// this function.
   613  	sem.Acquire(context.Background(), 1)
   614  	defer sem.Release(1)
   615  	if cipdBinary == "" {
   616  		c <- packageFloatingRef{
   617  			pkg:      pkg,
   618  			err:      errors.New("cipd is not bootstrapped when calling checkFloatingRefs"),
   619  			floating: false,
   620  		}
   621  		return
   622  	}
   623  	// jsonFile will be cleaned up by caller.
   624  	jsonFile, err := ioutil.TempFile(jsonDir, "cipd*.json")
   625  	if err != nil {
   626  		c <- packageFloatingRef{
   627  			pkg:      pkg,
   628  			err:      err,
   629  			floating: false,
   630  		}
   631  		return
   632  	}
   633  	jsonFileName := jsonFile.Name()
   634  	jsonFile.Close()
   635  
   636  	// Remove ${platform}, ${os} ... from package name before calling cipd describe
   637  	// as it will fail when these tags are not compatible with current host.
   638  	pkgName := pkg.PackageName
   639  	if MustExpand(pkgName) {
   640  		expandedPkgName, err := Expand(pkgName, plats)
   641  		if err != nil {
   642  			c <- packageFloatingRef{
   643  				pkg:      pkg,
   644  				err:      err,
   645  				floating: false,
   646  			}
   647  			return
   648  		}
   649  		if len(expandedPkgName) == 0 {
   650  			c <- packageFloatingRef{
   651  				pkg: pkg,
   652  				// avoid using %q as we don't want escape characters in the output.
   653  				err:      fmt.Errorf("cannot expand package \"%s\"", pkgName),
   654  				floating: false,
   655  			}
   656  			return
   657  		}
   658  		pkgName = expandedPkgName[0]
   659  	}
   660  
   661  	args := []string{"describe", pkgName, "-version", pkg.VersionTag, "-json-output", jsonFileName}
   662  	jirix.Logger.Debugf("Invoke cipd with %v", args)
   663  
   664  	var stdoutBuf bytes.Buffer
   665  	var stderrBuf bytes.Buffer
   666  	command := exec.Command(cipdBinary, args...)
   667  	command.Env = append(os.Environ(), "CIPD_HTTP_USER_AGENT_PREFIX="+getUserAgent())
   668  	command.Stdin = os.Stdin
   669  	command.Stdout = &stdoutBuf
   670  	command.Stderr = &stderrBuf
   671  
   672  	if err := command.Run(); err != nil {
   673  		c <- packageFloatingRef{
   674  			pkg:      pkg,
   675  			err:      fmt.Errorf("cipd describe failed due to error: %v, stdout: %s\n, stderr: %s", err, stdoutBuf.String(), stderrBuf.String()),
   676  			floating: false,
   677  		}
   678  		return
   679  	}
   680  
   681  	jsonData, err := ioutil.ReadFile(jsonFileName)
   682  	if err != nil {
   683  		c <- packageFloatingRef{
   684  			pkg:      pkg,
   685  			err:      err,
   686  			floating: false,
   687  		}
   688  		return
   689  	}
   690  	// Example of generated JSON:
   691  	// {
   692  	// 	"result": {
   693  	// 	  "pin": {
   694  	// 		"package": "gn/gn/linux-amd64",
   695  	// 		"instance_id": "4usiirrra6WbnCKgplRoiJ8EcAsCuqCOd_7tpf_yXrAC"
   696  	// 	  },
   697  	// 	  "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com",
   698  	// 	  "registered_ts": 1554328925,
   699  	// 	  "refs": [
   700  	// 		{
   701  	// 		  "ref": "latest",
   702  	// 		  "instance_id": "4usiirrra6WbnCKgplRoiJ8EcAsCuqCOd_7tpf_yXrAC",
   703  	// 		  "modified_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com",
   704  	// 		  "modified_ts": 1554328926
   705  	// 		}
   706  	// 	  ],
   707  	// 	  "tags": [
   708  	// 		{
   709  	// 		  "tag": "git_repository:https://gn.googlesource.com/gn",
   710  	// 		  "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com",
   711  	// 		  "registered_ts": 1554328925
   712  	// 		},
   713  	// 		{
   714  	// 		  "tag": "git_revision:64b846c96daeb3eaf08e26d8a84d8451c6cb712b",
   715  	// 		  "registered_by": "user:infra-internal-gn-builder@chops-service-accounts.iam.gserviceaccount.com",
   716  	// 		  "registered_ts": 1554328925
   717  	// 		}
   718  	// 	  ]
   719  	// 	}
   720  	// }
   721  	// Only "refs" is needed.
   722  
   723  	var result struct {
   724  		Result describeJSON `json:"result"`
   725  	}
   726  
   727  	if err := json.Unmarshal(jsonData, &result); err != nil {
   728  		c <- packageFloatingRef{
   729  			pkg:      pkg,
   730  			err:      err,
   731  			floating: false,
   732  		}
   733  		return
   734  	}
   735  
   736  	for _, v := range result.Result.Refs {
   737  		if v.Ref == pkg.VersionTag {
   738  			c <- packageFloatingRef{pkg: pkg, err: nil, floating: true}
   739  			return
   740  		}
   741  	}
   742  	c <- packageFloatingRef{pkg: pkg, err: nil, floating: false}
   743  	return
   744  }
   745  
   746  // Platform contains the parameters for a "${platform}" template.
   747  // The string value can be obtained by calling String().
   748  type Platform struct {
   749  	// OS defines the operating system of this platform. It can be any OS
   750  	// supported by golang.
   751  	OS string
   752  	// Arch defines the CPU architecture of this platform. It can be any
   753  	// architecture supported by golang.
   754  	Arch string
   755  }
   756  
   757  // NewPlatform parses a platform string into Platform struct.
   758  func NewPlatform(s string) (Platform, error) {
   759  	fields := strings.Split(s, "-")
   760  	if len(fields) != 2 {
   761  		return Platform{"", ""}, fmt.Errorf("illegal platform %q", s)
   762  	}
   763  	return Platform{fields[0], fields[1]}, nil
   764  }
   765  
   766  // String generates a string represents the Platform in "OS"-"Arch" form.
   767  func (p Platform) String() string {
   768  	return p.OS + "-" + p.Arch
   769  }
   770  
   771  // Expander returns an Expander populated with p's fields.
   772  func (p Platform) Expander() Expander {
   773  	return Expander{
   774  		"os":       p.OS,
   775  		"arch":     p.Arch,
   776  		"platform": p.String(),
   777  	}
   778  }
   779  
   780  // Expander is a mapping of simple string substitutions which is used to
   781  // expand cipd package name templates. For example:
   782  //
   783  //   ex, err := template.Expander{
   784  //     "platform": "mac-amd64"
   785  //   }.Expand("foo/${platform}")
   786  //
   787  // `ex` would be "foo/mac-amd64".
   788  type Expander map[string]string
   789  
   790  // Expand applies package template expansion rules to the package template,
   791  //
   792  // If err == ErrSkipTemplate, that means that this template does not apply to
   793  // this os/arch combination and should be skipped.
   794  //
   795  // The expansion rules are as follows:
   796  //   - "some text" will pass through unchanged
   797  //   - "${variable}" will directly substitute the given variable
   798  //   - "${variable=val1,val2}" will substitute the given variable, if its value
   799  //     matches one of the values in the list of values. If the current value
   800  //     does not match, this returns ErrSkipTemplate.
   801  //
   802  // Attempting to expand an unknown variable is an error.
   803  // After expansion, any lingering '$' in the template is an error.
   804  func (t Expander) Expand(template string) (pkg string, err error) {
   805  	skip := false
   806  
   807  	pkg = templateRE.ReplaceAllStringFunc(template, func(parm string) string {
   808  		// ${...}
   809  		contents := parm[2 : len(parm)-1]
   810  
   811  		varNameValues := strings.SplitN(contents, "=", 2)
   812  		if len(varNameValues) == 1 {
   813  			// ${varName}
   814  			if value, ok := t[varNameValues[0]]; ok {
   815  				return value
   816  			}
   817  
   818  			err = fmt.Errorf("unknown variable in ${%s}", contents)
   819  		}
   820  
   821  		// ${varName=value,value}
   822  		ourValue, ok := t[varNameValues[0]]
   823  		if !ok {
   824  			err = fmt.Errorf("unknown variable %q", parm)
   825  			return parm
   826  		}
   827  
   828  		for _, val := range strings.Split(varNameValues[1], ",") {
   829  			if val == ourValue {
   830  				return ourValue
   831  			}
   832  		}
   833  		skip = true
   834  		return parm
   835  	})
   836  	if skip {
   837  		err = ErrSkipTemplate
   838  	}
   839  	if err == nil && strings.ContainsRune(pkg, '$') {
   840  		err = fmt.Errorf("unable to process some variables in %q", template)
   841  	}
   842  	return
   843  }
   844  
   845  // Expand method expands a cipdPath that contains templates such as ${platform}
   846  // into concrete full paths. It might return an empty slice if platforms
   847  // do not match the requirements in cipdPath.
   848  func Expand(cipdPath string, platforms []Platform) ([]string, error) {
   849  	output := make([]string, 0)
   850  	//expanders := make([]Expander, 0)
   851  	if !MustExpand(cipdPath) {
   852  		output = append(output, cipdPath)
   853  		return output, nil
   854  	}
   855  
   856  	for _, plat := range platforms {
   857  		pkg, err := plat.Expander().Expand(cipdPath)
   858  		if err == ErrSkipTemplate {
   859  			continue
   860  		}
   861  		if err != nil {
   862  			return nil, err
   863  		}
   864  		output = append(output, pkg)
   865  	}
   866  	return output, nil
   867  }
   868  
   869  // Decl method expands a cipdPath that contains ${platform}, ${os}, ${arch}
   870  // with information in platforms. Unlike the Expand method which
   871  // returns a list of expanded cipd paths, the Decl method only returns a
   872  // single path containing all platforms. For example, if platforms contain
   873  // "linux-amd64" and "linux-arm64", ${platform} will be replaced to
   874  // ${platform=linux-amd64,linux-arm64}. This is a workaround for a limitation
   875  // in 'cipd ensure-file-resolve' which requires the header of '.ensure' file
   876  // to contain all available platforms. But in some cases, a package may miss
   877  // a particular platform, which will cause a crash on this cipd command. By
   878  // explicitly list all supporting platforms in the cipdPath, we can avoid
   879  // crashing cipd.
   880  func Decl(cipdPath string, platforms []Platform) (string, error) {
   881  	if !MustExpand(cipdPath) || len(platforms) == 0 {
   882  		return cipdPath, nil
   883  	}
   884  
   885  	osMap := make(map[string]bool)
   886  	platMap := make(map[string]bool)
   887  	archMap := make(map[string]bool)
   888  
   889  	replacedOS := "${os="
   890  	replacedArch := "${arch="
   891  	replacedPlat := "${platform="
   892  
   893  	for _, plat := range platforms {
   894  		if _, ok := osMap[plat.OS]; !ok {
   895  			replacedOS += plat.OS + ","
   896  			osMap[plat.OS] = true
   897  		}
   898  		if _, ok := archMap[plat.Arch]; !ok {
   899  			replacedArch += plat.Arch + ","
   900  			archMap[plat.Arch] = true
   901  		}
   902  		if _, ok := platMap[plat.String()]; !ok {
   903  			replacedPlat += plat.String() + ","
   904  			platMap[plat.String()] = true
   905  		}
   906  	}
   907  	replacedOS = replacedOS[:len(replacedOS)-1] + "}"
   908  	replacedArch = replacedArch[:len(replacedArch)-1] + "}"
   909  	replacedPlat = replacedPlat[:len(replacedPlat)-1] + "}"
   910  
   911  	cipdPath = strings.Replace(cipdPath, "${os}", replacedOS, -1)
   912  	cipdPath = strings.Replace(cipdPath, "${arch}", replacedArch, -1)
   913  	cipdPath = strings.Replace(cipdPath, "${platform}", replacedPlat, -1)
   914  	return cipdPath, nil
   915  }
   916  
   917  // MustExpand checks if template usages such as "${platform}" exist
   918  // in cipdPath. If they exist, this function will return true. Otherwise
   919  // it returns false.
   920  func MustExpand(cipdPath string) bool {
   921  	return templateRE.MatchString(cipdPath)
   922  }
   923  
   924  // DefaultPlatforms returns a slice of Platform objects that are currently
   925  // validated by jiri.
   926  func DefaultPlatforms() []Platform {
   927  	return []Platform{
   928  		Platform{"linux", "amd64"},
   929  		Platform{"mac", "amd64"},
   930  	}
   931  }