github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/commands/pip/install.go (about)

     1  package pip
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    10  	piputils "github.com/jfrog/jfrog-cli-core/artifactory/utils/pip"
    11  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/pip/dependencies"
    12  	"github.com/jfrog/jfrog-cli-core/utils/config"
    13  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    14  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    15  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    16  	"github.com/jfrog/jfrog-client-go/utils/log"
    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, ServerDetails: 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, -1, 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, Type: buildinfo.Pip, Dependencies: projectDependencies}
   107  	modules = append(modules, module)
   108  
   109  	buildInfo.Modules = modules
   110  	utils.SaveBuildInfo(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber, pic.buildConfiguration.Project, 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  		log.Debug(fmt.Sprintf("Using build name: %s as module name.", moduleName))
   129  	}
   130  
   131  	pic.buildConfiguration.Module = moduleName
   132  	return nil
   133  }
   134  
   135  func (pic *PipInstallCommand) prepare() (pythonExecutablePath string, err error) {
   136  	log.Debug("Preparing prerequisites.")
   137  
   138  	pythonExecutablePath, err = piputils.GetExecutablePath("python")
   139  	if err != nil {
   140  		return
   141  	}
   142  
   143  	pic.args, pic.buildConfiguration, err = utils.ExtractBuildDetailsFromArgs(pic.args)
   144  	if err != nil {
   145  		return
   146  	}
   147  
   148  	// Prepare build-info.
   149  	if pic.buildConfiguration.BuildName != "" && pic.buildConfiguration.BuildNumber != "" {
   150  		pic.shouldCollectBuildInfo = true
   151  		if err = utils.SaveBuildGeneralDetails(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber, pic.buildConfiguration.Project); err != nil {
   152  			return
   153  		}
   154  	}
   155  
   156  	return
   157  }
   158  
   159  func getPackageName(pythonExecutablePath string, pipArgs []string) (string, error) {
   160  	// Build uses setup.py file.
   161  	// Setup.py should be in current dir.
   162  	filePath, err := getSetupPyFilePath()
   163  	if err != nil || filePath == "" {
   164  		// Error was returned or setup.py does not exist in directory.
   165  		return "", err
   166  	}
   167  
   168  	// Extract package name from setup.py.
   169  	packageName, err := piputils.ExtractPackageNameFromSetupPy(filePath, pythonExecutablePath)
   170  	if err != nil {
   171  		// If setup.py egg_info command failed we use build name as module name and continue to pip-install execution
   172  		log.Info("Couldn't determine module-name after running the 'egg_info' command: " + err.Error())
   173  		return "", nil
   174  	}
   175  	return packageName, err
   176  }
   177  
   178  // Look for 'setup.py' file in current work dir.
   179  // If found, return its absolute path.
   180  func getSetupPyFilePath() (string, error) {
   181  	wd, err := os.Getwd()
   182  	if errorutils.CheckError(err) != nil {
   183  		return "", err
   184  	}
   185  
   186  	filePath := filepath.Join(wd, "setup.py")
   187  	// Check if setup.py exists.
   188  	validPath, err := fileutils.IsFileExists(filePath, false)
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  	if !validPath {
   193  		log.Debug("Could not find setup.py file in current directory:", wd)
   194  		return "", nil
   195  	}
   196  
   197  	return filePath, nil
   198  }
   199  
   200  func (pic *PipInstallCommand) cleanBuildInfoDir() {
   201  	if err := utils.RemoveBuildDir(pic.buildConfiguration.BuildName, pic.buildConfiguration.BuildNumber, pic.buildConfiguration.Project); err != nil {
   202  		log.Error(fmt.Sprintf("Failed cleaning build-info directory: %s", err.Error()))
   203  	}
   204  }
   205  
   206  func promptMissingDependencies(missingDeps []string) {
   207  	if len(missingDeps) > 0 {
   208  		log.Warn(strings.Join(missingDeps, "\n"))
   209  		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" +
   210  			"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.")
   211  	}
   212  }
   213  
   214  func (pic *PipInstallCommand) CommandName() string {
   215  	return "rt_pip_install"
   216  }
   217  
   218  func (pic *PipInstallCommand) ServerDetails() (*config.ServerDetails, error) {
   219  	return pic.rtDetails, nil
   220  }