blob: 0c8d3d8fec9c968e81bc7051757f7b2a09776cb2 [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 (
"bytes"
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/proxydir"
)
// Modules is the exporter that produces module layouts.
// Each "repository" is put in its 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 := os.ReadFile(gomod)
if err != nil {
return err
}
if err := os.WriteFile(gomod+".temp", contents, 0644); err != nil {
return err
}
}
exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod")
var primaryGomod bytes.Buffer
fmt.Fprintf(&primaryGomod, "module %s\nrequire (\n", exported.primary)
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
}
fmt.Fprintf(&primaryGomod, "\t%v %v\n", other, version)
}
fmt.Fprintf(&primaryGomod, ")\n")
if err := os.WriteFile(filepath.Join(primaryDir, "go.mod"), primaryGomod.Bytes(), 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 := os.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.
modProxyDir := 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
}
if err := writeModuleFiles(modProxyDir, 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"),
"GOMODCACHE=",
"GOPROXY="+proxydir.ToURL(modProxyDir),
"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", "all"},
Env: exported.Config.Env,
BuildFlags: exported.Config.BuildFlags,
WorkingDir: exported.Config.Dir,
}
_, err := new(gocommand.Runner).Run(context.Background(), inv)
return err
}
func writeModuleFiles(rootDir, module, ver string, filePaths map[string]string) error {
fileData := make(map[string][]byte)
for name, path := range filePaths {
contents, err := os.ReadFile(path)
if err != nil {
return err
}
fileData[name] = contents
}
return proxydir.WriteModuleVersion(rootDir, module, ver, fileData)
}
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"
}