// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "fmt" "io" "io/ioutil" "os" "path/filepath" "sort" "strings" "time" "golang.org/x/mod/module" "golang.org/x/mod/semver" "golang.org/x/mod/zip" "golang.org/x/tools/txtar" ) // buildProxyDir constructs a temporary directory suitable for use as a // module proxy with a file:// URL. The caller is responsible for deleting // the directory when it's no longer needed. func buildProxyDir() (proxyDir, proxyURL string, err error) { proxyDir, err = ioutil.TempDir("", "gorelease-proxy") if err != nil { return "", "", err } defer func(proxyDir string) { if err != nil { os.RemoveAll(proxyDir) } }(proxyDir) txtarPaths, err := filepath.Glob(filepath.FromSlash("testdata/mod/*.txt")) if err != nil { return "", "", err } versionLists := make(map[string][]string) for _, txtarPath := range txtarPaths { base := filepath.Base(txtarPath) stem := base[:len(base)-len(".txt")] i := strings.LastIndexByte(base, '_') if i < 0 { return "", "", fmt.Errorf("invalid module archive: %s", base) } modPath := strings.ReplaceAll(stem[:i], "_", "/") version := stem[i+1:] versionLists[modPath] = append(versionLists[modPath], version) modDir := filepath.Join(proxyDir, modPath, "@v") if err := os.MkdirAll(modDir, 0777); err != nil { return "", "", err } arc, err := txtar.ParseFile(txtarPath) if err != nil { return "", "", err } isCanonical := version == module.CanonicalVersion(version) var zipContents []zip.File var haveInfo, haveMod bool var goMod txtar.File for _, af := range arc.Files { if !isCanonical && af.Name != ".info" { return "", "", fmt.Errorf("%s: version is non-canonical but contains files other than .info", txtarPath) } if af.Name == ".info" || af.Name == ".mod" { if af.Name == ".info" { haveInfo = true } else { haveMod = true } outPath := filepath.Join(modDir, version+af.Name) if err := ioutil.WriteFile(outPath, af.Data, 0666); err != nil { return "", "", err } continue } if af.Name == "go.mod" { goMod = af } zipContents = append(zipContents, txtarFile{af}) } if !isCanonical && !haveInfo { return "", "", fmt.Errorf("%s: version is non-canonical but does not have .info", txtarPath) } if !haveInfo { outPath := filepath.Join(modDir, version+".info") outContent := fmt.Sprintf(`{"Version":"%s"}`, version) if err := ioutil.WriteFile(outPath, []byte(outContent), 0666); err != nil { return "", "", err } } if !haveMod && goMod.Name != "" { outPath := filepath.Join(modDir, version+".mod") if err := ioutil.WriteFile(outPath, goMod.Data, 0666); err != nil { return "", "", err } } if len(zipContents) > 0 { zipPath := filepath.Join(modDir, version+".zip") zipFile, err := os.Create(zipPath) if err != nil { return "", "", err } defer zipFile.Close() if err := zip.Create(zipFile, module.Version{Path: modPath, Version: version}, zipContents); err != nil { return "", "", err } if err := zipFile.Close(); err != nil { return "", "", err } } } buf := &bytes.Buffer{} for modPath, versions := range versionLists { outPath := filepath.Join(proxyDir, modPath, "@v", "list") sort.Slice(versions, func(i, j int) bool { return semver.Compare(versions[i], versions[j]) < 0 }) for _, v := range versions { fmt.Fprintln(buf, v) } if err := ioutil.WriteFile(outPath, buf.Bytes(), 0666); err != nil { return "", "", err } buf.Reset() } // Make sure the URL path starts with a slash on Windows. Absolute paths // normally start with a drive letter. // TODO(golang.org/issue/32456): use url.FromFilePath when implemented. if strings.HasPrefix(proxyDir, "/") { proxyURL = "file://" + proxyDir } else { proxyURL = "file:///" + filepath.FromSlash(proxyDir) } return proxyDir, proxyURL, nil } type txtarFile struct { f txtar.File } func (f txtarFile) Path() string { return f.f.Name } func (f txtarFile) Lstat() (os.FileInfo, error) { return txtarFileInfo{f.f}, nil } func (f txtarFile) Open() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(f.f.Data)), nil } type txtarFileInfo struct { f txtar.File } func (f txtarFileInfo) Name() string { return f.f.Name } func (f txtarFileInfo) Size() int64 { return int64(len(f.f.Data)) } func (f txtarFileInfo) Mode() os.FileMode { return 0444 } func (f txtarFileInfo) ModTime() time.Time { return time.Time{} } func (f txtarFileInfo) IsDir() bool { return false } func (f txtarFileInfo) Sys() interface{} { return nil } func extractTxtar(destDir string, arc *txtar.Archive) error { for _, f := range arc.Files { outPath := filepath.Join(destDir, f.Name) if err := os.MkdirAll(filepath.Dir(outPath), 0777); err != nil { return err } if err := ioutil.WriteFile(outPath, f.Data, 0666); err != nil { return err } } return nil }