github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/commands/pip/install.go (about) 1 package pip 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/jfrog/jfrog-cli-go/artifactory/utils" 7 piputils "github.com/jfrog/jfrog-cli-go/artifactory/utils/pip" 8 "github.com/jfrog/jfrog-cli-go/artifactory/utils/pip/dependencies" 9 "github.com/jfrog/jfrog-cli-go/utils/config" 10 "github.com/jfrog/jfrog-client-go/artifactory/buildinfo" 11 "github.com/jfrog/jfrog-client-go/utils/errorutils" 12 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 13 "github.com/jfrog/jfrog-client-go/utils/log" 14 "os" 15 "path/filepath" 16 "strings" 17 ) 18 19 type PipInstallCommand struct { 20 *PipCommand 21 buildConfiguration *utils.BuildConfiguration 22 shouldCollectBuildInfo bool 23 } 24 25 func NewPipInstallCommand() *PipInstallCommand { 26 return &PipInstallCommand{PipCommand: &PipCommand{}} 27 } 28 29 func (pic *PipInstallCommand) Run() error { 30 log.Info("Running pip Install.") 31 32 pythonExecutablePath, err := pic.prepare() 33 if err != nil { 34 return err 35 } 36 37 pipInstaller := &piputils.PipInstaller{Args: pic.args, RtDetails: pic.rtDetails, Repository: pic.repository, ShouldParseLogs: pic.shouldCollectBuildInfo} 38 err = pipInstaller.Install() 39 if err != nil { 40 pic.cleanBuildInfoDir() 41 return err 42 } 43 44 if !pic.shouldCollectBuildInfo { 45 log.Info("pip install finished successfully.") 46 return nil 47 } 48 49 // Collect build-info. 50 if err := pic.collectBuildInfo(pythonExecutablePath, pipInstaller.DependencyToFileMap); err != nil { 51 pic.cleanBuildInfoDir() 52 return err 53 } 54 55 log.Info("pip install finished successfully.") 56 return nil 57 } 58 59 func (pic *PipInstallCommand) collectBuildInfo(pythonExecutablePath string, dependencyToFileMap map[string]string) error { 60 if err := pic.determineModuleName(pythonExecutablePath); err != nil { 61 return err 62 } 63 64 allDependencies := pic.getAllDependencies(dependencyToFileMap) 65 dependenciesCache, err := dependencies.GetProjectDependenciesCache() 66 if err != nil { 67 return err 68 } 69 70 // Populate dependencies information - checksums and file-name. 71 servicesManager, err := utils.CreateServiceManager(pic.rtDetails, false) 72 if err != nil { 73 return err 74 } 75 missingDeps, err := dependencies.AddDepsInfoAndReturnMissingDeps(allDependencies, dependenciesCache, dependencyToFileMap, servicesManager, pic.repository) 76 if err != nil { 77 return err 78 } 79 80 promptMissingDependencies(missingDeps) 81 dependencies.UpdateDependenciesCache(allDependencies) 82 pic.saveBuildInfo(allDependencies) 83 return nil 84 } 85 86 // Convert dependencyToFileMap to Dependencies map. 87 func (pic *PipInstallCommand) getAllDependencies(dependencyToFileMap map[string]string) map[string]*buildinfo.Dependency { 88 dependenciesMap := make(map[string]*buildinfo.Dependency, len(dependencyToFileMap)) 89 for depName := range dependencyToFileMap { 90 dependenciesMap[depName] = &buildinfo.Dependency{Id: depName} 91 } 92 93 return dependenciesMap 94 } 95 96 func (pic *PipInstallCommand) saveBuildInfo(allDependencies map[string]*buildinfo.Dependency) { 97 buildInfo := &buildinfo.BuildInfo{} 98 var modules []buildinfo.Module 99 var projectDependencies []buildinfo.Dependency 100 101 for _, dep := range allDependencies { 102 projectDependencies = append(projectDependencies, *dep) 103 } 104 105 // Save build-info. 106 module := buildinfo.Module{Id: pic.buildConfiguration.Module, Dependencies: projectDependencies} 107 modules = append(modules, module) 108 109 buildInfo.Modules = modules 110 utils.SaveBuildInfo(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber, buildInfo) 111 } 112 113 func (pic *PipInstallCommand) determineModuleName(pythonExecutablePath string) error { 114 // If module-name was set in command, don't change it. 115 if pic.buildConfiguration.Module != "" { 116 return nil 117 } 118 119 // Get package-name. 120 moduleName, err := getPackageName(pythonExecutablePath, pic.args) 121 if err != nil { 122 return err 123 } 124 125 // If package-name unknown, set module as build-name. 126 if moduleName == "" { 127 moduleName = pic.buildConfiguration.BuildName 128 } 129 130 pic.buildConfiguration.Module = moduleName 131 return nil 132 } 133 134 func (pic *PipInstallCommand) prepare() (pythonExecutablePath string, err error) { 135 log.Debug("Preparing prerequisites.") 136 137 pythonExecutablePath, err = piputils.GetExecutablePath("python") 138 if err != nil { 139 return 140 } 141 142 pic.args, pic.buildConfiguration, err = utils.ExtractBuildDetailsFromArgs(pic.args) 143 if err != nil { 144 return 145 } 146 147 // Prepare build-info. 148 if pic.buildConfiguration.BuildName != "" && pic.buildConfiguration.BuildNumber != "" { 149 pic.shouldCollectBuildInfo = true 150 if err = utils.SaveBuildGeneralDetails(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber); err != nil { 151 return 152 } 153 } 154 155 return 156 } 157 158 func getPackageName(pythonExecutablePath string, pipArgs []string) (string, error) { 159 // Check if using requirements file. 160 isRequirementsFileUsed, err := isCommandUsesRequirementsFile(pipArgs) 161 if err != nil { 162 return "", err 163 } 164 if isRequirementsFileUsed { 165 return "", nil 166 } 167 168 // Build uses setup.py file. 169 // Setup.py should be in current dir. 170 filePath, err := getSetuppyFilePath() 171 if err != nil { 172 return "", err 173 } 174 175 if filePath == "" { 176 // Couldn't resolve requirements file or setup.py. 177 return "", errorutils.CheckError(errors.New("Could not find installation file for pip command, the command must include '--requirement' or be executed from within the directory containing the 'setup.py' file.")) 178 } 179 180 // Extract package name from setup.py. 181 packageName, err := piputils.ExtractPackageNameFromSetupPy(filePath, pythonExecutablePath) 182 if err != nil { 183 return "", errors.New("Failed determining module-name from 'setup.py' file: " + err.Error()) 184 } 185 return packageName, err 186 } 187 188 // Look for 'requirements' flag in command args. 189 // If found, validate the file exists and return its path. 190 func isCommandUsesRequirementsFile(args []string) (bool, error) { 191 // Get requirements flag args. 192 _, _, requirementsFilePath, err := utils.FindFlagFirstMatch([]string{"-r", "--requirement"}, args) 193 if err != nil || requirementsFilePath == "" { 194 // Args don't include a path to requirements file. 195 return false, err 196 } 197 198 return true, nil 199 } 200 201 // Look for 'setup.py' file in current work dir. 202 // If found, return its absolute path. 203 func getSetuppyFilePath() (string, error) { 204 wd, err := os.Getwd() 205 if errorutils.CheckError(err) != nil { 206 return "", err 207 } 208 209 filePath := filepath.Join(wd, "setup.py") 210 // Check if setup.py exists. 211 validPath, err := fileutils.IsFileExists(filePath, false) 212 if err != nil { 213 return "", err 214 } 215 if !validPath { 216 return "", errorutils.CheckError(errors.New(fmt.Sprintf("Could not find setup.py file in current directory: %s", wd))) 217 } 218 219 return filePath, nil 220 } 221 222 func (pic *PipInstallCommand) cleanBuildInfoDir() { 223 if err := utils.RemoveBuildDir(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber); err != nil { 224 log.Error(fmt.Sprintf("Failed cleaning build-info directory: %s", err.Error())) 225 } 226 } 227 228 func promptMissingDependencies(missingDeps []string) { 229 if len(missingDeps) > 0 { 230 log.Warn(strings.Join(missingDeps, "\n")) 231 log.Warn("The pypi packages above could not be found in Artifactory or were not downloaded in this execution, therefore they are not included in the build-info.\n" + 232 "Reinstalling in clean environment or using '--no-cache-dir' and '--force-reinstall' flags (in one execution only), will force downloading and populating Artifactory with these packages, and therefore resolve the issue.") 233 } 234 } 235 236 func (pic *PipInstallCommand) CommandName() string { 237 return "rt_pip_install" 238 } 239 240 func (pic *PipInstallCommand) RtDetails() (*config.ArtifactoryDetails, error) { 241 return pic.rtDetails, nil 242 }