golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/gorelease/proxy_test.go (about)

     1  // Copyright 2019 The Go 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 main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"golang.org/x/mod/module"
    18  	"golang.org/x/mod/semver"
    19  	"golang.org/x/mod/zip"
    20  	"golang.org/x/tools/txtar"
    21  )
    22  
    23  // buildProxyDir constructs a temporary directory suitable for use as a
    24  // module proxy with a file:// URL. The caller is responsible for deleting
    25  // the directory when it's no longer needed.
    26  //
    27  // proxyVersions must be a map of module version true. If proxyVersions is
    28  // empty, all modules in mod/ will be included in the proxy list. If proxy
    29  // versions is non-empty, only those modules in mod/ that match an entry in
    30  // proxyVersions will be included.
    31  func buildProxyDir(proxyVersions map[module.Version]bool, tests []*test) (proxyDir, proxyURL string, err error) {
    32  	proxyDir, err = os.MkdirTemp("", "gorelease-proxy")
    33  	if err != nil {
    34  		return "", "", err
    35  	}
    36  
    37  	txtarPaths, err := filepath.Glob(filepath.FromSlash("testdata/mod/*.txt"))
    38  	if err != nil {
    39  		return "", "", err
    40  	}
    41  
    42  	// Map of modPath to versions for that modPath.
    43  	versionLists := make(map[string][]string)
    44  
    45  	for _, t := range tests {
    46  		versionLists[t.modPath] = []string{}
    47  		modDir := filepath.Join(proxyDir, t.modPath, "@v")
    48  		if err := os.MkdirAll(modDir, 0777); err != nil {
    49  			return "", "", err
    50  		}
    51  	}
    52  
    53  	for _, txtarPath := range txtarPaths {
    54  		base := filepath.Base(txtarPath)
    55  		stem := base[:len(base)-len(".txt")]
    56  		i := strings.LastIndexByte(base, '_')
    57  		if i < 0 {
    58  			return "", "", fmt.Errorf("invalid module archive: %s", base)
    59  		}
    60  		modPath := strings.ReplaceAll(stem[:i], "_", "/")
    61  		version := stem[i+1:]
    62  		mv := module.Version{
    63  			Path:    modPath,
    64  			Version: version,
    65  		}
    66  
    67  		// User has supplied proxyVersions. Honor proxy versions by only
    68  		// accepting those versions supplied in proxyVersions.
    69  		if len(proxyVersions) > 0 {
    70  			if !proxyVersions[mv] {
    71  				// modPath@version is not in proxyVersions: skip.
    72  				continue
    73  			}
    74  		}
    75  
    76  		versionLists[modPath] = append(versionLists[modPath], version)
    77  
    78  		modDir := filepath.Join(proxyDir, modPath, "@v")
    79  		if err := os.MkdirAll(modDir, 0777); err != nil {
    80  			return "", "", err
    81  		}
    82  
    83  		arc, err := txtar.ParseFile(txtarPath)
    84  		if err != nil {
    85  			return "", "", err
    86  		}
    87  
    88  		isCanonical := version == module.CanonicalVersion(version)
    89  		var zipContents []zip.File
    90  		var haveInfo, haveMod bool
    91  		var goMod txtar.File
    92  		for _, af := range arc.Files {
    93  			if !isCanonical && af.Name != ".info" {
    94  				return "", "", fmt.Errorf("%s: version is non-canonical but contains files other than .info", txtarPath)
    95  			}
    96  			if af.Name == ".info" || af.Name == ".mod" {
    97  				if af.Name == ".info" {
    98  					haveInfo = true
    99  				} else {
   100  					haveMod = true
   101  				}
   102  				outPath := filepath.Join(modDir, version+af.Name)
   103  				if err := os.WriteFile(outPath, af.Data, 0666); err != nil {
   104  					return "", "", err
   105  				}
   106  				continue
   107  			}
   108  			if af.Name == "go.mod" {
   109  				goMod = af
   110  			}
   111  
   112  			zipContents = append(zipContents, txtarFile{af})
   113  		}
   114  		if !isCanonical && !haveInfo {
   115  			return "", "", fmt.Errorf("%s: version is non-canonical but does not have .info", txtarPath)
   116  		}
   117  
   118  		if !haveInfo {
   119  			outPath := filepath.Join(modDir, version+".info")
   120  			outContent := fmt.Sprintf(`{"Version":"%s"}`, version)
   121  			if err := os.WriteFile(outPath, []byte(outContent), 0666); err != nil {
   122  				return "", "", err
   123  			}
   124  		}
   125  		if !haveMod && goMod.Name != "" {
   126  			outPath := filepath.Join(modDir, version+".mod")
   127  			if err := os.WriteFile(outPath, goMod.Data, 0666); err != nil {
   128  				return "", "", err
   129  			}
   130  		}
   131  
   132  		if len(zipContents) > 0 {
   133  			zipPath := filepath.Join(modDir, version+".zip")
   134  			zipFile, err := os.Create(zipPath)
   135  			if err != nil {
   136  				return "", "", err
   137  			}
   138  			defer zipFile.Close()
   139  			if err := zip.Create(zipFile, module.Version{Path: modPath, Version: version}, zipContents); err != nil {
   140  				return "", "", err
   141  			}
   142  			if err := zipFile.Close(); err != nil {
   143  				return "", "", err
   144  			}
   145  		}
   146  	}
   147  
   148  	buf := &bytes.Buffer{}
   149  	for modPath, versions := range versionLists {
   150  		outPath := filepath.Join(proxyDir, modPath, "@v", "list")
   151  		sort.Slice(versions, func(i, j int) bool {
   152  			return semver.Compare(versions[i], versions[j]) < 0
   153  		})
   154  		for _, v := range versions {
   155  			fmt.Fprintln(buf, v)
   156  		}
   157  		if err := os.WriteFile(outPath, buf.Bytes(), 0666); err != nil {
   158  			return "", "", err
   159  		}
   160  		buf.Reset()
   161  	}
   162  
   163  	// Make sure the URL path starts with a slash on Windows. Absolute paths
   164  	// normally start with a drive letter.
   165  	// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
   166  	if strings.HasPrefix(proxyDir, "/") {
   167  		proxyURL = "file://" + proxyDir
   168  	} else {
   169  		proxyURL = "file:///" + filepath.FromSlash(proxyDir)
   170  	}
   171  	return proxyDir, proxyURL, nil
   172  }
   173  
   174  type txtarFile struct {
   175  	f txtar.File
   176  }
   177  
   178  func (f txtarFile) Path() string                { return f.f.Name }
   179  func (f txtarFile) Lstat() (os.FileInfo, error) { return txtarFileInfo{f.f}, nil }
   180  func (f txtarFile) Open() (io.ReadCloser, error) {
   181  	return io.NopCloser(bytes.NewReader(f.f.Data)), nil
   182  }
   183  
   184  type txtarFileInfo struct {
   185  	f txtar.File
   186  }
   187  
   188  func (f txtarFileInfo) Name() string       { return f.f.Name }
   189  func (f txtarFileInfo) Size() int64        { return int64(len(f.f.Data)) }
   190  func (f txtarFileInfo) Mode() os.FileMode  { return 0444 }
   191  func (f txtarFileInfo) ModTime() time.Time { return time.Time{} }
   192  func (f txtarFileInfo) IsDir() bool        { return false }
   193  func (f txtarFileInfo) Sys() interface{}   { return nil }
   194  
   195  func extractTxtar(destDir string, arc *txtar.Archive) error {
   196  	for _, f := range arc.Files {
   197  		outPath := filepath.Join(destDir, f.Name)
   198  		if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil {
   199  			return err
   200  		}
   201  		if err := os.WriteFile(outPath, f.Data, 0666); err != nil {
   202  			return err
   203  		}
   204  	}
   205  	return nil
   206  }