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 }