blob: 8bf830ad4f917e57cbba3b95c549886f1ceef9f7 [file] [log] [blame]
// Copyright 2018 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 packagestest
import (
"archive/zip"
"context"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"golang.org/x/tools/internal/gocommand"
)
// Modules is the exporter that produces module layouts.
// Each "repository" is put in it's own module, and the module file generated
// will have replace directives for all other modules.
// Given the two files
// golang.org/repoa#a/a.go
// golang.org/repob#b/b.go
// You would get the directory layout
// /sometemporarydirectory
// ├── repoa
// │ ├── a
// │ │ └── a.go
// │ └── go.mod
// └── repob
// ├── b
// │ └── b.go
// └── go.mod
// and the working directory would be
// /sometemporarydirectory/repoa
var Modules = modules{}
type modules struct{}
type moduleAtVersion struct {
module string
version string
}
func (modules) Name() string {
return "Modules"
}
func (modules) Filename(exported *Exported, module, fragment string) string {
if module == exported.primary {
return filepath.Join(primaryDir(exported), fragment)
}
return filepath.Join(moduleDir(exported, module), fragment)
}
func (modules) Finalize(exported *Exported) error {
// Write out the primary module. This module can use symlinks and
// other weird stuff, and will be the working dir for the go command.
// It depends on all the other modules.
primaryDir := primaryDir(exported)
if err := os.MkdirAll(primaryDir, 0755); err != nil {
return err
}
exported.Config.Dir = primaryDir
if exported.written[exported.primary] == nil {
exported.written[exported.primary] = make(map[string]string)
}
// Create a map of modulepath -> {module, version} for modulepaths
// that are of the form `repoa/mod1@v1.1.0`.
versions := make(map[string]moduleAtVersion)
for module := range exported.written {
if splt := strings.Split(module, "@"); len(splt) > 1 {
versions[module] = moduleAtVersion{
module: splt[0],
version: splt[1],
}
}
}
// If the primary module already has a go.mod, write the contents to a temp
// go.mod for now and then we will reset it when we are getting all the markers.
if gomod := exported.written[exported.primary]["go.mod"]; gomod != "" {
contents, err := ioutil.ReadFile(gomod)
if err != nil {
return err
}
if err := ioutil.WriteFile(gomod+".temp", contents, 0644); err != nil {
return err
}
}
exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod")
primaryGomod := "module " + exported.primary + "\nrequire (\n"
for other := range exported.written {
if other == exported.primary {
continue
}
version := moduleVersion(other)
// If other is of the form `repo1/mod1@v1.1.0`,
// then we need to extract the module and the version.
if v, ok := versions[other]; ok {
other = v.module
version = v.version
}
primaryGomod += fmt.Sprintf("\t%v %v\n", other, version)
}
primaryGomod += ")\n"
if err := ioutil.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 0644); err != nil {
return err
}
// Create the mod cache so we can rename it later, even if we don't need it.
if err := os.MkdirAll(modCache(exported), 0755); err != nil {
return err
}
// Write out the go.mod files for the other modules.
for module, files := range exported.written {
if module == exported.primary {
continue
}
dir := moduleDir(exported, module)
modfile := filepath.Join(dir, "go.mod")
// If other is of the form `repo1/mod1@v1.1.0`,
// then we need to extract the module name without the version.
if v, ok := versions[module]; ok {
module = v.module
}
if err := ioutil.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil {
return err
}
files["go.mod"] = modfile
}
// Zip up all the secondary modules into the proxy dir.
proxyDir := filepath.Join(exported.temp, "modproxy")
for module, files := range exported.written {
if module == exported.primary {
continue
}
version := moduleVersion(module)
// If other is of the form `repo1/mod1@v1.1.0`,
// then we need to extract the module and the version.
if v, ok := versions[module]; ok {
module = v.module
version = v.version
}
dir := filepath.Join(proxyDir, module, "@v")
if err := writeModuleProxy(dir, module, version, files); err != nil {
return fmt.Errorf("creating module proxy dir for %v: %v", module, err)
}
}
// Discard the original mod cache dir, which contained the files written
// for us by Export.
if err := os.Rename(modCache(exported), modCache(exported)+".orig"); err != nil {
return err
}
exported.Config.Env = append(exported.Config.Env,
"GO111MODULE=on",
"GOPATH="+filepath.Join(exported.temp, "modcache"),
"GOPROXY="+proxyDirToURL(proxyDir),
"GOSUMDB=off",
)
// Run go mod download to recreate the mod cache dir with all the extra
// stuff in cache. All the files created by Export should be recreated.
inv := gocommand.Invocation{
Verb: "mod",
Args: []string{"download"},
Env: exported.Config.Env,
BuildFlags: exported.Config.BuildFlags,
WorkingDir: exported.Config.Dir,
}
if _, err := inv.Run(context.Background()); err != nil {
return err
}
return nil
}
// writeModuleProxy creates a directory in the proxy dir for a module.
func writeModuleProxy(dir, module, ver string, files map[string]string) error {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// the modproxy checks for versions by looking at the "list" file,
// since we are supporting multiple versions, create the file if it does not exist or
// append the version number to the preexisting file.
f, err := os.OpenFile(filepath.Join(dir, "list"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
if _, err := f.WriteString(ver + "\n"); err != nil {
return err
}
// go.mod, copied from the file written in Finalize.
modContents, err := ioutil.ReadFile(files["go.mod"])
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(dir, ver+".mod"), modContents, 0644); err != nil {
return err
}
// info file, just the bare bones.
infoContents := []byte(fmt.Sprintf(`{"Version": "%v", "Time":"2017-12-14T13:08:43Z"}`, ver))
if err := ioutil.WriteFile(filepath.Join(dir, ver+".info"), infoContents, 0644); err != nil {
return err
}
// zip of all the source files.
f, err = os.OpenFile(filepath.Join(dir, ver+".zip"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
z := zip.NewWriter(f)
for name, path := range files {
zf, err := z.Create(module + "@" + ver + "/" + name)
if err != nil {
return err
}
contents, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if _, err := zf.Write(contents); err != nil {
return err
}
}
if err := z.Close(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return nil
}
func modCache(exported *Exported) string {
return filepath.Join(exported.temp, "modcache/pkg/mod")
}
func primaryDir(exported *Exported) string {
return filepath.Join(exported.temp, path.Base(exported.primary))
}
func moduleDir(exported *Exported, module string) string {
if strings.Contains(module, "@") {
return filepath.Join(modCache(exported), module)
}
return filepath.Join(modCache(exported), path.Dir(module), path.Base(module)+"@"+moduleVersion(module))
}
var versionSuffixRE = regexp.MustCompile(`v\d+`)
func moduleVersion(module string) string {
if versionSuffixRE.MatchString(path.Base(module)) {
return path.Base(module) + ".0.0"
}
return "v1.0.0"
}