github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/utils/nuget/dependencies/packagesconfig.go (about)

     1  package dependencies
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	gofrogcmd "github.com/jfrog/gofrog/io"
     7  	"github.com/jfrog/jfrog-cli-go/artifactory/utils/nuget"
     8  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
     9  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    10  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/log"
    12  	"io/ioutil"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  var packagesFilePath = "packages.config"
    18  
    19  // Register packages.config extractor
    20  func init() {
    21  	register(&packagesExtractor{})
    22  }
    23  
    24  // packages.config dependency extractor
    25  type packagesExtractor struct {
    26  	allDependencies  map[string]*buildinfo.Dependency
    27  	childrenMap      map[string][]string
    28  	rootDependencies []string
    29  }
    30  
    31  func (extractor *packagesExtractor) IsCompatible(projectName, projectRoot string) (bool, error) {
    32  	packagesConfigPath := filepath.Join(projectRoot, packagesFilePath)
    33  	exists, err := fileutils.IsFileExists(packagesConfigPath, false)
    34  	if exists {
    35  		log.Debug("Found", packagesConfigPath, "file for project:", projectName)
    36  		return true, err
    37  	}
    38  	return false, err
    39  }
    40  
    41  func (extractor *packagesExtractor) DirectDependencies() ([]string, error) {
    42  	return getDirectDependencies(extractor.allDependencies, extractor.childrenMap), nil
    43  }
    44  
    45  func (extractor *packagesExtractor) AllDependencies() (map[string]*buildinfo.Dependency, error) {
    46  	return extractor.allDependencies, nil
    47  }
    48  
    49  func (extractor *packagesExtractor) ChildrenMap() (map[string][]string, error) {
    50  	return extractor.childrenMap, nil
    51  }
    52  
    53  // Create new packages.config extractor
    54  func (extractor *packagesExtractor) new(projectName, projectRoot string) (Extractor, error) {
    55  	newExtractor := &packagesExtractor{allDependencies: map[string]*buildinfo.Dependency{}, childrenMap: map[string][]string{}}
    56  	packagesConfig, err := newExtractor.loadPackagesConfig(projectRoot)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	globalPackagesCache, err := newExtractor.getGlobalPackagesCache()
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	err = newExtractor.extract(packagesConfig, globalPackagesCache)
    67  	return newExtractor, err
    68  }
    69  
    70  func (extractor *packagesExtractor) extract(packagesConfig *packagesConfig, globalPackagesCache string) error {
    71  	for _, nuget := range packagesConfig.XmlPackages {
    72  		id := strings.ToLower(nuget.Id)
    73  		nPackage := &nugetPackage{id: id, version: nuget.Version, dependencies: map[string]bool{}}
    74  		// First lets check if the original version exists within the file system:
    75  		pack, err := createNugetPackage(globalPackagesCache, nuget, nPackage)
    76  		if err != nil {
    77  			return err
    78  		}
    79  		if pack == nil {
    80  			// If doesn't exists lets build the array of alternative versions.
    81  			alternativeVersions := createAlternativeVersionForms(nuget.Version)
    82  			// Now lets do a loop to run over the alternative possibilities
    83  			for i := 0; i < len(alternativeVersions); i++ {
    84  				nPackage.version = alternativeVersions[i]
    85  				pack, err = createNugetPackage(globalPackagesCache, nuget, nPackage)
    86  				if err != nil {
    87  					return err
    88  				}
    89  				if pack != nil {
    90  					break
    91  				}
    92  			}
    93  		}
    94  		if pack != nil {
    95  			extractor.allDependencies[id] = pack.dependency
    96  			extractor.childrenMap[id] = pack.getDependencies()
    97  		} else {
    98  			log.Warn(fmt.Sprintf("The following NuGet package %s with version %s was not found in the NuGet cache %s and therefore not"+
    99  				" added to the dependecy tree. This might be because the package already exists in a different NuGet cache,"+
   100  				" possibly the SDK cache. Removing the package from this cache may resolve the issue", nuget.Id, nuget.Version, globalPackagesCache))
   101  		}
   102  	}
   103  	return nil
   104  }
   105  
   106  // NuGet allows the version will be with missing or unnecessary zeros
   107  // This method will return a list of the possible alternative versions
   108  // "1.0" --> []string{"1.0.0.0", "1.0.0", "1"}
   109  // "1" --> []string{"1.0.0.0", "1.0.0", "1.0"}
   110  // "1.2" --> []string{"1.2.0.0", "1.2.0"}
   111  // "1.22.33" --> []string{"1.22.33.0"}
   112  // "1.22.33.44" --> []string{}
   113  // "1.0.2" --> []string{"1.0.2.0"}
   114  func createAlternativeVersionForms(originalVersion string) []string {
   115  	versionSlice := strings.Split(originalVersion, ".")
   116  	versionSliceSize := len(versionSlice)
   117  	for i := 4; i > versionSliceSize; i-- {
   118  		versionSlice = append(versionSlice, "0")
   119  	}
   120  
   121  	var alternativeVersions []string
   122  
   123  	for i := 4; i > 0; i-- {
   124  		version := strings.Join(versionSlice[:i], ".")
   125  		if version != originalVersion {
   126  			alternativeVersions = append(alternativeVersions, version)
   127  		}
   128  		if !strings.HasSuffix(version, ".0") {
   129  			return alternativeVersions
   130  		}
   131  	}
   132  	return alternativeVersions
   133  }
   134  
   135  func (extractor *packagesExtractor) loadPackagesConfig(rootPath string) (*packagesConfig, error) {
   136  	packagesFilePath := filepath.Join(rootPath, packagesFilePath)
   137  	content, err := ioutil.ReadFile(packagesFilePath)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	config := &packagesConfig{}
   143  	err = xml.Unmarshal(content, config)
   144  	if err != nil {
   145  		return nil, errorutils.CheckError(err)
   146  	}
   147  	return config, nil
   148  }
   149  
   150  type dfsHelper struct {
   151  	visited  bool
   152  	notRoot  bool
   153  	circular bool
   154  }
   155  
   156  func getDirectDependencies(allDependencies map[string]*buildinfo.Dependency, childrenMap map[string][]string) []string {
   157  	helper := map[string]*dfsHelper{}
   158  	for id := range allDependencies {
   159  		helper[id] = &dfsHelper{}
   160  	}
   161  
   162  	for id := range allDependencies {
   163  		if helper[id].visited {
   164  			continue
   165  		}
   166  		searchRootDependencies(helper, id, allDependencies, childrenMap, map[string]bool{id: true})
   167  	}
   168  	var rootDependencies []string
   169  	for id, nodeData := range helper {
   170  		if !nodeData.notRoot || nodeData.circular {
   171  			rootDependencies = append(rootDependencies, id)
   172  		}
   173  	}
   174  
   175  	return rootDependencies
   176  }
   177  
   178  func searchRootDependencies(dfsHelper map[string]*dfsHelper, currentId string, allDependencies map[string]*buildinfo.Dependency, childrenMap map[string][]string, traversePath map[string]bool) {
   179  	if dfsHelper[currentId].visited {
   180  		return
   181  	}
   182  	for _, next := range childrenMap[currentId] {
   183  		if _, ok := allDependencies[next]; !ok {
   184  			// No such dependency
   185  			continue
   186  		}
   187  		if traversePath[next] {
   188  			for circular := range traversePath {
   189  				dfsHelper[circular].circular = true
   190  			}
   191  			continue
   192  		}
   193  
   194  		// Not root dependency
   195  		dfsHelper[next].notRoot = true
   196  		traversePath[next] = true
   197  		searchRootDependencies(dfsHelper, next, allDependencies, childrenMap, traversePath)
   198  		delete(traversePath, next)
   199  	}
   200  	dfsHelper[currentId].visited = true
   201  }
   202  
   203  func createNugetPackage(packagesPath string, nuget xmlPackage, nPackage *nugetPackage) (*nugetPackage, error) {
   204  	nupkgPath := filepath.Join(packagesPath, nPackage.id, nPackage.version, strings.Join([]string{nPackage.id, nPackage.version, "nupkg"}, "."))
   205  
   206  	exists, err := fileutils.IsFileExists(nupkgPath, false)
   207  
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	if !exists {
   213  		return nil, nil
   214  	}
   215  
   216  	fileDetails, err := fileutils.GetFileDetails(nupkgPath)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	nPackage.dependency = &buildinfo.Dependency{Id: nuget.Id + ":" + nuget.Version, Checksum: &buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}}
   221  
   222  	// Nuspec file that holds the metadata for the package.
   223  	nuspecPath := filepath.Join(packagesPath, nPackage.id, nPackage.version, strings.Join([]string{nPackage.id, "nuspec"}, "."))
   224  	nuspecContent, err := ioutil.ReadFile(nuspecPath)
   225  	if err != nil {
   226  		return nil, errorutils.CheckError(err)
   227  	}
   228  
   229  	nuspec := &nuspec{}
   230  	err = xml.Unmarshal(nuspecContent, nuspec)
   231  	if err != nil {
   232  		pack := nPackage.id + ":" + nPackage.version
   233  		log.Warn("Package:", pack, "couldn't be parsed due to:", err.Error(), ". Skipping the package dependency.")
   234  		return nPackage, nil
   235  	}
   236  
   237  	for _, dependency := range nuspec.Metadata.Dependencies.Dependencies {
   238  		nPackage.dependencies[strings.ToLower(dependency.Id)] = true
   239  	}
   240  
   241  	for _, group := range nuspec.Metadata.Dependencies.Groups {
   242  		for _, dependency := range group.Dependencies {
   243  			nPackage.dependencies[strings.ToLower(dependency.Id)] = true
   244  		}
   245  	}
   246  
   247  	return nPackage, nil
   248  }
   249  
   250  type nugetPackage struct {
   251  	id           string
   252  	version      string
   253  	dependency   *buildinfo.Dependency
   254  	dependencies map[string]bool // Set of dependencies
   255  }
   256  
   257  func (nugetPackage *nugetPackage) getDependencies() []string {
   258  	var dependencies []string
   259  	for key := range nugetPackage.dependencies {
   260  		dependencies = append(dependencies, key)
   261  	}
   262  
   263  	return dependencies
   264  }
   265  
   266  func (extractor *packagesExtractor) getGlobalPackagesCache() (string, error) {
   267  	localsCmd, err := nuget.NewNugetCmd()
   268  	if err != nil {
   269  		return "", err
   270  	}
   271  	//nuget locals global-packages -list
   272  	localsCmd.Command = []string{"locals", "global-packages"}
   273  	localsCmd.CommandFlags = []string{"-list"}
   274  
   275  	output, err := gofrogcmd.RunCmdOutput(localsCmd)
   276  	if err != nil {
   277  		return "", err
   278  	}
   279  
   280  	globalPackagesPath := strings.TrimSpace(strings.TrimPrefix(output, "global-packages:"))
   281  	exists, err := fileutils.IsDirExists(globalPackagesPath, false)
   282  	if err != nil {
   283  		return "", err
   284  	}
   285  	if !exists {
   286  		return "", errorutils.CheckError(fmt.Errorf("Could not find global packages path at: %s", globalPackagesPath))
   287  	}
   288  	return globalPackagesPath, nil
   289  }
   290  
   291  // packages.config xml objects for unmarshalling
   292  type packagesConfig struct {
   293  	XMLName     xml.Name     `xml:"packages"`
   294  	XmlPackages []xmlPackage `xml:"package"`
   295  }
   296  
   297  type xmlPackage struct {
   298  	Id      string `xml:"id,attr"`
   299  	Version string `xml:"version,attr"`
   300  }
   301  
   302  type nuspec struct {
   303  	XMLName  xml.Name `xml:"package"`
   304  	Metadata metadata `xml:"metadata"`
   305  }
   306  
   307  type metadata struct {
   308  	Dependencies xmlDependencies `xml:"dependencies"`
   309  }
   310  
   311  type xmlDependencies struct {
   312  	Groups       []group      `xml:"group"`
   313  	Dependencies []xmlPackage `xml:"dependency"`
   314  }
   315  
   316  type group struct {
   317  	TargetFramework string       `xml:"targetFramework,attr"`
   318  	Dependencies    []xmlPackage `xml:"dependency"`
   319  }