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  }