github.com/please-build/puku@v1.7.3-0.20240516143641-f7d7f4941f57/generate/deps.go (about)

     1  package generate
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/please-build/buildtools/build"
    10  
    11  	"github.com/please-build/puku/config"
    12  	"github.com/please-build/puku/fs"
    13  	"github.com/please-build/puku/kinds"
    14  	"github.com/please-build/puku/knownimports"
    15  )
    16  
    17  // resolveImport resolves an import path to a build target. It will return an empty string if the import is for a pkg in
    18  // the go sdk. Otherwise, it will return the build target for that dependency, or an error if it can't be resolved. If
    19  // the target can be resolved to a module that isn't currently added to this project, it will return the build target,
    20  // and record the new module in `u.newModules`. These should later be written to the build graph.
    21  func (u *updater) resolveImport(conf *config.Config, i string) (string, error) {
    22  	if t, ok := u.resolvedImports[i]; ok {
    23  		return t, nil
    24  	}
    25  
    26  	if t := conf.GetKnownTarget(i); t != "" {
    27  		return t, nil
    28  	}
    29  
    30  	t, err := u.reallyResolveImport(conf, i)
    31  	if err == nil {
    32  		u.resolvedImports[i] = t
    33  	}
    34  	return t, err
    35  }
    36  
    37  // reallyResolveImport actually does the resolution of an import path to a build target.
    38  func (u *updater) reallyResolveImport(conf *config.Config, i string) (string, error) {
    39  	if knownimports.IsInGoRoot(i) {
    40  		return "", nil
    41  	}
    42  
    43  	if t := u.installs.Get(i); t != "" {
    44  		return t, nil
    45  	}
    46  
    47  	thirdPartyDir := conf.GetThirdPartyDir()
    48  
    49  	// Check to see if the target exists in the current repo
    50  	if fs.IsSubdir(u.plzConf.ImportPath(), i) || u.plzConf.ImportPath() == "" {
    51  		t, err := u.localDep(i)
    52  		if err != nil {
    53  			return "", err
    54  		}
    55  
    56  		if t != "" {
    57  			return t, nil
    58  		}
    59  		// The above isSubdir check only checks the import path. Modules can have import paths that contain the
    60  		// current module, so we should carry on here in case we can resolve this to a third party module
    61  	}
    62  
    63  	t := depTarget(u.modules, i, thirdPartyDir)
    64  	if t != "" {
    65  		return t, nil
    66  	}
    67  
    68  	// If we're using go_module, we can't automatically add new modules to the graph so we should give up here.
    69  	if u.usingGoModule {
    70  		return "", fmt.Errorf("module not found")
    71  	}
    72  
    73  	log.Infof("Resolving module for %v...", i)
    74  
    75  	// Otherwise try and resolve it to a new dep via the module proxy. We assume the module will contain the package.
    76  	// Please will error out in a reasonable way if it doesn't.
    77  	// TODO it would be more correct to download the module and check it actually contains the package
    78  	mod, err := u.proxy.ResolveModuleForPackage(i)
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  
    83  	log.Infof("Resolved to %v... done", mod.Module)
    84  
    85  	// If the package belongs to this module, we should have found this package when resolving local imports above. We
    86  	// don't want to resolve this like a third party module, so we should return an error here.
    87  	if mod.Module == u.plzConf.ImportPath() {
    88  		return "", fmt.Errorf("can't find import %q", i)
    89  	}
    90  
    91  	u.newModules = append(u.newModules, mod)
    92  	u.modules = append(u.modules, mod.Module)
    93  
    94  	// TODO we can probably shortcut this and assume the target is in the above module
    95  	t = depTarget(u.modules, i, thirdPartyDir)
    96  	if t != "" {
    97  		return t, nil
    98  	}
    99  
   100  	return "", fmt.Errorf("module not found")
   101  }
   102  
   103  // isInScope returns true when the given path is in scope of the current run i.e. if we are going to format the BUILD
   104  // file there.
   105  func (u *updater) isInScope(path string) bool {
   106  	for _, p := range u.paths {
   107  		if p == path {
   108  			return true
   109  		}
   110  	}
   111  	return false
   112  }
   113  
   114  // localDep finds a dependency local to this repository, checking the BUILD file for a go_library target. Returns an
   115  // empty string when no target is found.
   116  func (u *updater) localDep(importPath string) (string, error) {
   117  	path := strings.Trim(strings.TrimPrefix(importPath, u.plzConf.ImportPath()), "/")
   118  	// If we're using GOPATH based resolution, we don't have a prefix to base whether a path is package local or not. In
   119  	// this case, we need to check if the directory exists. If it doesn't it's not a local import.
   120  	if _, err := os.Lstat(path); os.IsNotExist(err) {
   121  		return "", nil
   122  	}
   123  	file, err := u.graph.LoadFile(path)
   124  	if err != nil {
   125  		return "", fmt.Errorf("failed to parse BUILD files in %v: %v", path, err)
   126  	}
   127  
   128  	conf, err := config.ReadConfig(path)
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  
   133  	var libTargets []*build.Rule
   134  	for _, rule := range file.Rules("") {
   135  		kind := conf.GetKind(rule.Kind())
   136  		if kind == nil {
   137  			continue
   138  		}
   139  
   140  		if kind.Type == kinds.Lib {
   141  			libTargets = append(libTargets, rule)
   142  		}
   143  	}
   144  
   145  	// If we can't find the lib target, and the target package is in scope for us to potentially generate it, check if
   146  	// we are going to generate it.
   147  	if len(libTargets) != 0 {
   148  		return BuildTarget(libTargets[0].Name(), path, ""), nil
   149  	}
   150  
   151  	if !u.isInScope(importPath) {
   152  		return "", fmt.Errorf("resolved %v to a local package, but no library target was found and it's not in scope to generate the target", importPath)
   153  	}
   154  
   155  	files, err := ImportDir(path)
   156  	if err != nil {
   157  		if os.IsNotExist(err) {
   158  			return "", nil
   159  		}
   160  		return "", fmt.Errorf("failed to import %v: %v", path, err)
   161  	}
   162  
   163  	// If there are any non-test sources, then we will generate a go_library here later on. Return that target name.
   164  	for _, f := range files {
   165  		if !f.IsTest() {
   166  			return BuildTarget(filepath.Base(importPath), path, ""), nil
   167  		}
   168  	}
   169  	return "", nil
   170  }
   171  
   172  func depTarget(modules []string, importPath, thirdPartyFolder string) string {
   173  	module := moduleForPackage(modules, importPath)
   174  	if module == "" {
   175  		// If we can't find this import, we can return nothing and the build rule will fail at build time reporting a
   176  		// sensible error. It may also be an import from the go SDK which is fine.
   177  		return ""
   178  	}
   179  
   180  	packageName := strings.TrimPrefix(strings.TrimPrefix(importPath, module), "/")
   181  	return SubrepoTarget(module, thirdPartyFolder, packageName)
   182  }
   183  
   184  func moduleForPackage(modules []string, importPath string) string {
   185  	module := ""
   186  	for _, mod := range modules {
   187  		ok := fs.IsSubdir(mod, importPath)
   188  		if ok && len(mod) > len(module) {
   189  			module = mod
   190  		}
   191  	}
   192  	return module
   193  }
   194  
   195  func SubrepoTarget(module, thirdPartyFolder, packageName string) string {
   196  	subrepoName := SubrepoName(module, thirdPartyFolder)
   197  
   198  	name := filepath.Base(packageName)
   199  	if packageName == "" {
   200  		name = filepath.Base(module)
   201  	}
   202  
   203  	return BuildTarget(name, packageName, subrepoName)
   204  }
   205  
   206  func SubrepoName(module, thirdPartyFolder string) string {
   207  	return filepath.Join(thirdPartyFolder, strings.ReplaceAll(module, "/", "_"))
   208  }
   209  
   210  func BuildTarget(name, pkgDir, subrepo string) string {
   211  	bs := new(strings.Builder)
   212  	if subrepo != "" {
   213  		bs.WriteString("///")
   214  		bs.WriteString(subrepo)
   215  	}
   216  
   217  	if pkgDir != "" || subrepo != "" {
   218  		bs.WriteString("//")
   219  	}
   220  
   221  	if pkgDir == "." {
   222  		pkgDir = ""
   223  	}
   224  
   225  	if pkgDir != "" {
   226  		bs.WriteString(pkgDir)
   227  		if filepath.Base(pkgDir) != name {
   228  			bs.WriteString(":")
   229  			bs.WriteString(name)
   230  		}
   231  	} else {
   232  		bs.WriteString(":")
   233  		bs.WriteString(name)
   234  	}
   235  	return bs.String()
   236  }