github.com/jfrog/build-info-go@v1.9.26/utils/pythonutils/piputils.go (about)

     1  package pythonutils
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/jfrog/build-info-go/utils"
    13  	"github.com/jfrog/gofrog/io"
    14  )
    15  
    16  // Executes the pip-dependency-map script and returns a dependency map of all the installed pip packages in the current environment to and another list of the top level dependencies
    17  func getPipDependencies(srcPath, dependenciesDirName string) (map[string][]string, []string, error) {
    18  	localPipdeptreeScript, err := getDepTreeScriptPath(dependenciesDirName)
    19  	if err != nil {
    20  		return nil, nil, err
    21  	}
    22  	localPipdeptree := io.NewCommand("python", "", []string{localPipdeptreeScript, "--json"})
    23  	localPipdeptree.Dir = srcPath
    24  	output, err := localPipdeptree.RunWithOutput()
    25  	if err != nil {
    26  		return nil, nil, err
    27  	}
    28  	// Parse into array.
    29  	packages := make([]pythonDependencyPackage, 0)
    30  	if err = json.Unmarshal(output, &packages); err != nil {
    31  		return nil, nil, err
    32  	}
    33  
    34  	return parseDependenciesToGraph(packages)
    35  }
    36  
    37  // Return path to the dependency-tree script, If it doesn't exist, it creates the file.
    38  func getDepTreeScriptPath(dependenciesDirName string) (string, error) {
    39  	if dependenciesDirName == "" {
    40  		home, err := os.UserHomeDir()
    41  		if err != nil {
    42  			return "", err
    43  		}
    44  		dependenciesDirName = filepath.Join(home, dependenciesDirName, "pip")
    45  	}
    46  	depTreeScriptName := "pipdeptree.py"
    47  	pipDependenciesPath := filepath.Join(dependenciesDirName, "pip", pipDepTreeVersion)
    48  	depTreeScriptPath := filepath.Join(pipDependenciesPath, depTreeScriptName)
    49  	err := writeScriptIfNeeded(pipDependenciesPath, depTreeScriptName)
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  	return depTreeScriptPath, err
    54  }
    55  
    56  // Creates local python script on jfrog dependencies path folder if such not exists
    57  func writeScriptIfNeeded(targetDirPath, scriptName string) error {
    58  	scriptPath := filepath.Join(targetDirPath, scriptName)
    59  	exists, err := utils.IsFileExists(scriptPath, false)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	if !exists {
    64  		err = os.MkdirAll(targetDirPath, os.ModeDir|os.ModePerm)
    65  		if err != nil {
    66  			return err
    67  		}
    68  		err = os.WriteFile(scriptPath, pipDepTreeContent, os.ModePerm)
    69  		if err != nil {
    70  			return err
    71  		}
    72  	}
    73  	return nil
    74  }
    75  
    76  func getPackageNameFromSetuppy(srcPath string) (string, error) {
    77  	filePath, err := getSetupPyFilePath(srcPath)
    78  	if err != nil || filePath == "" {
    79  		// Error was returned or setup.py does not exist in directory.
    80  		return "", err
    81  	}
    82  
    83  	// Extract package name from setup.py.
    84  	packageName, err := ExtractPackageNameFromSetupPy(filePath)
    85  	if err != nil {
    86  		// If setup.py egg_info command failed we use build name as module name and continue to pip-install execution
    87  		return "", errors.New("couldn't determine module-name after running the 'egg_info' command: " + err.Error())
    88  	}
    89  	return packageName, nil
    90  }
    91  
    92  // Look for 'setup.py' file in current work dir.
    93  // If found, return its absolute path.
    94  func getSetupPyFilePath(srcPath string) (string, error) {
    95  	return getFilePath(srcPath, "setup.py")
    96  }
    97  
    98  // Get the project-name by running 'egg_info' command on setup.py and extracting it from 'PKG-INFO' file.
    99  func ExtractPackageNameFromSetupPy(setuppyFilePath string) (string, error) {
   100  	// Execute egg_info command and return PKG-INFO content.
   101  	content, err := getEgginfoPkginfoContent(setuppyFilePath)
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  
   106  	// Extract project name from file content.
   107  	return getProjectIdFromFileContent(content)
   108  }
   109  
   110  // Run egg-info command on setup.py. The command generates metadata files.
   111  // Return the content of the 'PKG-INFO' file.
   112  func getEgginfoPkginfoContent(setuppyFilePath string) (output []byte, err error) {
   113  	eggBase, err := utils.CreateTempDir()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	defer func() {
   118  		err = errors.Join(err, utils.RemoveTempDir(eggBase))
   119  	}()
   120  
   121  	// Run python 'egg_info --egg-base <eggBase>' command.
   122  	var args []string
   123  	pythonExecutable, windowsPyArg := GetPython3Executable()
   124  	if windowsPyArg != "" {
   125  		args = append(args, windowsPyArg)
   126  	}
   127  	args = append(args, setuppyFilePath, "egg_info", "--egg-base", eggBase)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	if err = exec.Command(pythonExecutable, args...).Run(); err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	// Read PKG_INFO under <eggBase>/*.egg-info/PKG-INFO.
   136  	return extractPackageNameFromEggBase(eggBase)
   137  }
   138  
   139  func GetPython3Executable() (string, string) {
   140  	windowsPyArg := ""
   141  	pythonExecutable, _ := exec.LookPath("python3")
   142  	if pythonExecutable == "" {
   143  		if utils.IsWindows() {
   144  			// If the OS is Windows try using Py Launcher: 'py -3'
   145  			pythonExecutable, _ = exec.LookPath("py")
   146  			if pythonExecutable != "" {
   147  				windowsPyArg = "-3"
   148  			}
   149  		}
   150  		// Try using 'python' if 'python3'/'py' couldn't be found
   151  		if pythonExecutable == "" {
   152  			pythonExecutable = "python"
   153  		}
   154  	}
   155  	return pythonExecutable, windowsPyArg
   156  }
   157  
   158  // Parse the output of 'python egg_info' command, in order to find the path of generated file 'PKG-INFO'.
   159  func extractPackageNameFromEggBase(eggBase string) ([]byte, error) {
   160  	files, err := os.ReadDir(eggBase)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	for _, file := range files {
   165  		if strings.HasSuffix(file.Name(), ".egg-info") {
   166  			pkginfoPath := filepath.Join(eggBase, file.Name(), "PKG-INFO")
   167  			// Read PKG-INFO file.
   168  			pkginfoFileExists, err := utils.IsFileExists(pkginfoPath, false)
   169  			if err != nil {
   170  				return nil, err
   171  			}
   172  			if !pkginfoFileExists {
   173  				return nil, errors.New("file 'PKG-INFO' couldn't be found in its designated location: " + pkginfoPath)
   174  			}
   175  
   176  			return os.ReadFile(pkginfoPath)
   177  		}
   178  	}
   179  
   180  	return nil, errors.New("couldn't find pkg info files")
   181  }
   182  
   183  // Get package ID from PKG-INFO file content.
   184  // If pattern of package name of version not found, return an error.
   185  func getProjectIdFromFileContent(content []byte) (string, error) {
   186  	// Create package-name regexp.
   187  	packageNameRegexp := regexp.MustCompile(`(?m)^Name:\s(\w[\w-.]+)`)
   188  
   189  	// Find first nameMatch of packageNameRegexp.
   190  	nameMatch := packageNameRegexp.FindStringSubmatch(string(content))
   191  	if len(nameMatch) < 2 {
   192  		return "", errors.New("failed extracting package name from content")
   193  	}
   194  
   195  	// Create package-version regexp.
   196  	packageVersionRegexp := regexp.MustCompile(`(?m)^Version:\s(\w[\w-.]+)`)
   197  
   198  	// Find first match of packageNameRegexp.
   199  	versionMatch := packageVersionRegexp.FindStringSubmatch(string(content))
   200  	if len(versionMatch) < 2 {
   201  		return "", errors.New("failed extracting package version from content")
   202  	}
   203  
   204  	return nameMatch[1] + ":" + versionMatch[1], nil
   205  }