golang.org/x/tools@v0.21.0/go/packages/packagestest/modules.go (about)

     1  // Copyright 2018 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 packagestest
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"golang.org/x/tools/internal/gocommand"
    17  	"golang.org/x/tools/internal/proxydir"
    18  )
    19  
    20  // Modules is the exporter that produces module layouts.
    21  // Each "repository" is put in its own module, and the module file generated
    22  // will have replace directives for all other modules.
    23  // Given the two files
    24  //
    25  //	golang.org/repoa#a/a.go
    26  //	golang.org/repob#b/b.go
    27  //
    28  // You would get the directory layout
    29  //
    30  //	/sometemporarydirectory
    31  //	├── repoa
    32  //	│   ├── a
    33  //	│   │   └── a.go
    34  //	│   └── go.mod
    35  //	└── repob
    36  //	    ├── b
    37  //	    │   └── b.go
    38  //	    └── go.mod
    39  //
    40  // and the working directory would be
    41  //
    42  //	/sometemporarydirectory/repoa
    43  var Modules = modules{}
    44  
    45  type modules struct{}
    46  
    47  type moduleAtVersion struct {
    48  	module  string
    49  	version string
    50  }
    51  
    52  func (modules) Name() string {
    53  	return "Modules"
    54  }
    55  
    56  func (modules) Filename(exported *Exported, module, fragment string) string {
    57  	if module == exported.primary {
    58  		return filepath.Join(primaryDir(exported), fragment)
    59  	}
    60  	return filepath.Join(moduleDir(exported, module), fragment)
    61  }
    62  
    63  func (modules) Finalize(exported *Exported) error {
    64  	// Write out the primary module. This module can use symlinks and
    65  	// other weird stuff, and will be the working dir for the go command.
    66  	// It depends on all the other modules.
    67  	primaryDir := primaryDir(exported)
    68  	if err := os.MkdirAll(primaryDir, 0755); err != nil {
    69  		return err
    70  	}
    71  	exported.Config.Dir = primaryDir
    72  	if exported.written[exported.primary] == nil {
    73  		exported.written[exported.primary] = make(map[string]string)
    74  	}
    75  
    76  	// Create a map of modulepath -> {module, version} for modulepaths
    77  	// that are of the form `repoa/mod1@v1.1.0`.
    78  	versions := make(map[string]moduleAtVersion)
    79  	for module := range exported.written {
    80  		if splt := strings.Split(module, "@"); len(splt) > 1 {
    81  			versions[module] = moduleAtVersion{
    82  				module:  splt[0],
    83  				version: splt[1],
    84  			}
    85  		}
    86  	}
    87  
    88  	// If the primary module already has a go.mod, write the contents to a temp
    89  	// go.mod for now and then we will reset it when we are getting all the markers.
    90  	if gomod := exported.written[exported.primary]["go.mod"]; gomod != "" {
    91  		contents, err := os.ReadFile(gomod)
    92  		if err != nil {
    93  			return err
    94  		}
    95  		if err := os.WriteFile(gomod+".temp", contents, 0644); err != nil {
    96  			return err
    97  		}
    98  	}
    99  
   100  	exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod")
   101  	primaryGomod := "module " + exported.primary + "\nrequire (\n"
   102  	for other := range exported.written {
   103  		if other == exported.primary {
   104  			continue
   105  		}
   106  		version := moduleVersion(other)
   107  		// If other is of the form `repo1/mod1@v1.1.0`,
   108  		// then we need to extract the module and the version.
   109  		if v, ok := versions[other]; ok {
   110  			other = v.module
   111  			version = v.version
   112  		}
   113  		primaryGomod += fmt.Sprintf("\t%v %v\n", other, version)
   114  	}
   115  	primaryGomod += ")\n"
   116  	if err := os.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 0644); err != nil {
   117  		return err
   118  	}
   119  
   120  	// Create the mod cache so we can rename it later, even if we don't need it.
   121  	if err := os.MkdirAll(modCache(exported), 0755); err != nil {
   122  		return err
   123  	}
   124  
   125  	// Write out the go.mod files for the other modules.
   126  	for module, files := range exported.written {
   127  		if module == exported.primary {
   128  			continue
   129  		}
   130  		dir := moduleDir(exported, module)
   131  		modfile := filepath.Join(dir, "go.mod")
   132  		// If other is of the form `repo1/mod1@v1.1.0`,
   133  		// then we need to extract the module name without the version.
   134  		if v, ok := versions[module]; ok {
   135  			module = v.module
   136  		}
   137  		if err := os.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil {
   138  			return err
   139  		}
   140  		files["go.mod"] = modfile
   141  	}
   142  
   143  	// Zip up all the secondary modules into the proxy dir.
   144  	modProxyDir := filepath.Join(exported.temp, "modproxy")
   145  	for module, files := range exported.written {
   146  		if module == exported.primary {
   147  			continue
   148  		}
   149  		version := moduleVersion(module)
   150  		// If other is of the form `repo1/mod1@v1.1.0`,
   151  		// then we need to extract the module and the version.
   152  		if v, ok := versions[module]; ok {
   153  			module = v.module
   154  			version = v.version
   155  		}
   156  		if err := writeModuleFiles(modProxyDir, module, version, files); err != nil {
   157  			return fmt.Errorf("creating module proxy dir for %v: %v", module, err)
   158  		}
   159  	}
   160  
   161  	// Discard the original mod cache dir, which contained the files written
   162  	// for us by Export.
   163  	if err := os.Rename(modCache(exported), modCache(exported)+".orig"); err != nil {
   164  		return err
   165  	}
   166  	exported.Config.Env = append(exported.Config.Env,
   167  		"GO111MODULE=on",
   168  		"GOPATH="+filepath.Join(exported.temp, "modcache"),
   169  		"GOMODCACHE=",
   170  		"GOPROXY="+proxydir.ToURL(modProxyDir),
   171  		"GOSUMDB=off",
   172  	)
   173  
   174  	// Run go mod download to recreate the mod cache dir with all the extra
   175  	// stuff in cache. All the files created by Export should be recreated.
   176  	inv := gocommand.Invocation{
   177  		Verb:       "mod",
   178  		Args:       []string{"download", "all"},
   179  		Env:        exported.Config.Env,
   180  		BuildFlags: exported.Config.BuildFlags,
   181  		WorkingDir: exported.Config.Dir,
   182  	}
   183  	_, err := new(gocommand.Runner).Run(context.Background(), inv)
   184  	return err
   185  }
   186  
   187  func writeModuleFiles(rootDir, module, ver string, filePaths map[string]string) error {
   188  	fileData := make(map[string][]byte)
   189  	for name, path := range filePaths {
   190  		contents, err := os.ReadFile(path)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		fileData[name] = contents
   195  	}
   196  	return proxydir.WriteModuleVersion(rootDir, module, ver, fileData)
   197  }
   198  
   199  func modCache(exported *Exported) string {
   200  	return filepath.Join(exported.temp, "modcache/pkg/mod")
   201  }
   202  
   203  func primaryDir(exported *Exported) string {
   204  	return filepath.Join(exported.temp, path.Base(exported.primary))
   205  }
   206  
   207  func moduleDir(exported *Exported, module string) string {
   208  	if strings.Contains(module, "@") {
   209  		return filepath.Join(modCache(exported), module)
   210  	}
   211  	return filepath.Join(modCache(exported), path.Dir(module), path.Base(module)+"@"+moduleVersion(module))
   212  }
   213  
   214  var versionSuffixRE = regexp.MustCompile(`v\d+`)
   215  
   216  func moduleVersion(module string) string {
   217  	if versionSuffixRE.MatchString(path.Base(module)) {
   218  		return path.Base(module) + ".0.0"
   219  	}
   220  	return "v1.0.0"
   221  }