<|start_filename|>build/npm_test.go<|end_filename|>
package build

import (
	"path/filepath"
	"strconv"
	"testing"
	"time"

	testdatautils "github.com/jfrog/build-info-go/build/testdata"
	buildutils "github.com/jfrog/build-info-go/build/utils"
	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
	"github.com/stretchr/testify/assert"
)

var logger = utils.NewDefaultLogger(utils.INFO)

func TestGenerateBuildInfoForNpm(t *testing.T) {
	service := NewBuildInfoService()
	npmBuild, err := service.GetOrCreateBuild("build-info-go-test-npm", strconv.FormatInt(time.Now().Unix(), 10))
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, npmBuild.Clean())
	}()
	npmVersion, _, err := buildutils.GetNpmVersionAndExecPath(logger)
	assert.NoError(t, err)

	// Create npm project.
	path, err := filepath.Abs(filepath.Join(".", "testdata"))
	assert.NoError(t, err)
	projectPath, cleanup := testdatautils.CreateNpmTest(t, path, "project3", false, npmVersion)
	defer cleanup()

	// Install dependencies in the npm project.
	npmArgs := []string{"--cache=" + filepath.Join(projectPath, "tmpcache")}
	_, _, err = buildutils.RunNpmCmd("npm", projectPath, buildutils.Install, npmArgs, logger)
	assert.NoError(t, err)
	npmModule, err := npmBuild.AddNpmModule(projectPath)
	assert.NoError(t, err)
	npmModule.SetNpmArgs(npmArgs)
	err = npmModule.CalcDependencies()
	assert.NoError(t, err)
	buildInfo, err := npmBuild.ToBuildInfo()
	assert.NoError(t, err)

	// Verify results.
	expectedBuildInfoJson := filepath.Join(projectPath, "expected_npm_buildinfo.json")
	expectedBuildInfo := testdatautils.GetBuildInfo(t, expectedBuildInfoJson)
	match, err := entities.IsEqualModuleSlices(buildInfo.Modules, expectedBuildInfo.Modules)
	assert.NoError(t, err)
	if !match {
		testdatautils.PrintBuildInfoMismatch(t, expectedBuildInfo.Modules, buildInfo.Modules)
	}
}


<|start_filename|>build/yarn.go<|end_filename|>
package build

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	buildutils "github.com/jfrog/build-info-go/build/utils"
	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
	"github.com/jfrog/gofrog/version"
	"os"
	"os/exec"
	"strings"
)

const minSupportedYarnVersion = "2.4.0"

type YarnModule struct {
	containingBuild          *Build
	name                     string
	srcPath                  string
	executablePath           string
	yarnArgs                 []string
	traverseDependenciesFunc func(dependency *entities.Dependency) (bool, error)
	threads                  int
	packageInfo              *buildutils.PackageInfo
}

// Pass an empty string for srcPath to find the Yarn project in the working directory.
func newYarnModule(srcPath string, containingBuild *Build) (*YarnModule, error) {
	executablePath, err := getYarnExecutable()
	if err != nil {
		return nil, err
	}
	containingBuild.logger.Debug("Found Yarn executable at:", executablePath)
	err = validateYarnVersion(executablePath, srcPath)
	if err != nil {
		return nil, err
	}

	if srcPath == "" {
		wd, err := os.Getwd()
		if err != nil {
			return nil, err
		}
		srcPath, err = utils.FindFileInDirAndParents(wd, "package.json")
		if err != nil {
			return nil, err
		}
	}

	// Read module name
	packageInfo, err := buildutils.ReadPackageInfoFromPackageJson(srcPath, nil)
	if err != nil {
		return nil, err
	}
	name := packageInfo.BuildInfoModuleId()

	return &YarnModule{name: name, srcPath: srcPath, containingBuild: containingBuild, executablePath: executablePath, threads: 3, packageInfo: packageInfo, yarnArgs: []string{"install"}}, nil
}

// Build builds the project, collects its dependencies and saves them in the build-info module.
func (ym *YarnModule) Build() error {
	err := runYarnCommand(ym.executablePath, ym.srcPath, ym.yarnArgs...)
	if err != nil {
		return err
	}
	if !ym.containingBuild.buildNameAndNumberProvided() {
		return nil
	}
	dependenciesMap, err := ym.getDependenciesMap()
	if err != nil {
		return err
	}
	buildInfoDependencies, err := buildutils.TraverseDependencies(dependenciesMap, ym.traverseDependenciesFunc, ym.threads)
	if err != nil {
		return err
	}
	buildInfoModule := entities.Module{Id: ym.name, Type: entities.Npm, Dependencies: buildInfoDependencies}
	buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}
	return ym.containingBuild.SaveBuildInfo(buildInfo)
}

func (ym *YarnModule) getDependenciesMap() (map[string]*entities.Dependency, error) {
	// Run 'yarn info'
	responseStr, errStr, err := runInfo(ym.executablePath, ym.srcPath)
	// Some warnings and messages of Yarn are printed to stderr. They don't necessarily cause the command to fail, but we'd want to show them to the user.
	if len(errStr) > 0 {
		ym.containingBuild.logger.Warn("Some errors occurred while collecting dependencies info:\n" + errStr)
	}
	if err != nil {
		ym.containingBuild.logger.Warn("An error was thrown while collecting dependencies info:", err.Error())
		// A returned error doesn't necessarily mean that the operation totally failed. If, in addition, the response is empty, then it probably does.
		if responseStr == "" {
			return nil, err
		}
	}

	dependenciesMap := make(map[string]*YarnDependency)
	scanner := bufio.NewScanner(strings.NewReader(responseStr))
	packageName := ym.packageInfo.FullName()
	var root *YarnDependency

	for scanner.Scan() {
		var currDependency YarnDependency
		currDepBytes := scanner.Bytes()
		err = json.Unmarshal(currDepBytes, &currDependency)
		if err != nil {
			return nil, err
		}
		dependenciesMap[currDependency.Value] = &currDependency

		// Check whether this dependency's name starts with the package name (which means this is the root)
		if strings.HasPrefix(currDependency.Value, packageName+"@") {
			root = &currDependency
		}
	}

	buildInfoDependencies := make(map[string]*entities.Dependency)
	err = ym.appendDependencyRecursively(root, []string{}, dependenciesMap, buildInfoDependencies)
	return buildInfoDependencies, err
}

func (ym *YarnModule) appendDependencyRecursively(yarnDependency *YarnDependency, pathToRoot []string, yarnDependenciesMap map[string]*YarnDependency,
	buildInfoDependencies map[string]*entities.Dependency) error {
	name := yarnDependency.Name()
	var ver string
	if len(pathToRoot) == 0 {
		// The version of the local project returned from 'yarn info' is '0.0.0-use.local', but we need the version mentioned in package.json
		ver = ym.packageInfo.Version
	} else {
		ver = yarnDependency.Details.Version
	}
	id := name + ":" + ver

	// To avoid infinite loops in case of circular dependencies, the dependency won't be added if it's already in pathToRoot
	if stringsSliceContains(pathToRoot, id) {
		return nil
	}

	for _, dependencyPtr := range yarnDependency.Details.Dependencies {
		innerDepKey := getYarnDependencyKeyFromLocator(dependencyPtr.Locator)
		innerYarnDep, exist := yarnDependenciesMap[innerDepKey]
		if !exist {
			return fmt.Errorf("an error occurred while creating dependencies tree: dependency %s was not found", dependencyPtr.Locator)
		}
		err := ym.appendDependencyRecursively(innerYarnDep, append([]string{id}, pathToRoot...), yarnDependenciesMap,
			buildInfoDependencies)
		if err != nil {
			return err
		}
	}

	// The root project should not be added to the dependencies list
	if len(pathToRoot) == 0 {
		return nil
	}

	buildInfoDependency, exist := buildInfoDependencies[id]
	if !exist {
		buildInfoDependency = &entities.Dependency{Id: id}
		buildInfoDependencies[id] = buildInfoDependency
	}

	buildInfoDependency.RequestedBy = append(buildInfoDependency.RequestedBy, pathToRoot)
	return nil
}

func (ym *YarnModule) SetName(name string) {
	ym.name = name
}

func (ym *YarnModule) SetArgs(yarnArgs []string) {
	ym.yarnArgs = yarnArgs
}

func (ym *YarnModule) SetThreads(threads int) {
	ym.threads = threads
}

// SetTraverseDependenciesFunc gets a function to execute on all dependencies after their collection in Build(), before they're saved.
// This function needs to return a boolean value indicating whether to save this dependency in the build-info or not.
// This function might run asynchronously with different dependencies (if the threads amount setting is bigger than 1).
// If more than one error are returned from this function in different threads, only the first of them will be returned from Build().
func (ym *YarnModule) SetTraverseDependenciesFunc(traverseDependenciesFunc func(dependency *entities.Dependency) (bool, error)) {
	ym.traverseDependenciesFunc = traverseDependenciesFunc
}

func (ym *YarnModule) AddArtifacts(artifacts ...entities.Artifact) error {
	if !ym.containingBuild.buildNameAndNumberProvided() {
		return errors.New("a build name must be provided in order to add artifacts")
	}
	partial := &entities.Partial{ModuleId: ym.name, ModuleType: entities.Npm, Artifacts: artifacts}
	return ym.containingBuild.SavePartialBuildInfo(partial)
}

type YarnDependency struct {
	// The value is usually in this structure: @scope/package-name@npm:1.0.0
	Value   string         `json:"value,omitempty"`
	Details YarnDepDetails `json:"children,omitempty"`
}

func (yd *YarnDependency) Name() string {
	// Find the first index of '@', starting from position 1. In scoped dependencies (like '@jfrog/package-name@npm:1.2.3') we want to keep the first '@' as part of the name.
	atSignIndex := strings.Index(yd.Value[1:], "@") + 1
	return yd.Value[:atSignIndex]
}

type YarnDepDetails struct {
	Version      string                  `json:"Version,omitempty"`
	Dependencies []YarnDependencyPointer `json:"Dependencies,omitempty"`
}

type YarnDependencyPointer struct {
	Descriptor string `json:"descriptor,omitempty"`
	Locator    string `json:"locator,omitempty"`
}

func getYarnExecutable() (string, error) {
	yarnExecPath, err := exec.LookPath("yarn")
	if err != nil {
		return "", err
	}
	return yarnExecPath, nil
}

func validateYarnVersion(executablePath, srcPath string) error {
	yarnVersionStr, err := getVersion(executablePath, srcPath)
	if err != nil {
		return err
	}
	yarnVersion := version.NewVersion(yarnVersionStr)
	if yarnVersion.Compare(minSupportedYarnVersion) > 0 {
		return errors.New("Yarn must have version " + minSupportedYarnVersion + " or higher. The current version is: " + yarnVersionStr)
	}
	return nil
}

func getVersion(executablePath, srcPath string) (string, error) {
	command := exec.Command(executablePath, "--version")
	command.Dir = srcPath
	outBuffer := bytes.NewBuffer([]byte{})
	command.Stdout = outBuffer
	command.Stderr = os.Stderr
	err := command.Run()
	if _, ok := err.(*exec.ExitError); ok {
		err = errors.New(err.Error())
	}
	return strings.TrimSpace(outBuffer.String()), err
}

// Yarn dependency locator usually looks like this: package-name@npm:1.2.3, which is used as the key in the dependencies map.
// But sometimes it points to a virtual package, so it looks different: package-name@virtual:[ID of virtual package]#npm:1.2.3.
// In this case we need to omit the part of the virtual package ID, to get the key as it is found in the dependencies map.
func getYarnDependencyKeyFromLocator(yarnDepLocator string) string {
	virtualIndex := strings.Index(yarnDepLocator, "@virtual:")
	if virtualIndex == -1 {
		return yarnDepLocator
	}

	hashSignIndex := strings.LastIndex(yarnDepLocator, "#")
	return yarnDepLocator[:virtualIndex+1] + yarnDepLocator[hashSignIndex+1:]
}

func runInfo(executablePath, srcPath string) (outResult, errResult string, err error) {
	command := exec.Command(executablePath, "info", "--all", "--recursive", "--json")
	command.Dir = srcPath
	outBuffer := bytes.NewBuffer([]byte{})
	command.Stdout = outBuffer
	errBuffer := bytes.NewBuffer([]byte{})
	command.Stderr = errBuffer
	err = command.Run()
	errResult = errBuffer.String()
	if err != nil {
		if _, ok := err.(*exec.ExitError); ok {
			err = errors.New(err.Error())
		}
		return
	}
	outResult = strings.TrimSpace(outBuffer.String())
	return
}

func runYarnCommand(executablePath, srcPath string, args ...string) error {
	command := exec.Command(executablePath, args...)
	command.Dir = srcPath
	command.Stdout = os.Stderr
	command.Stderr = os.Stderr
	err := command.Run()
	if _, ok := err.(*exec.ExitError); ok {
		err = errors.New(err.Error())
	}
	return err
}

func stringsSliceContains(slice []string, str string) bool {
	for _, element := range slice {
		if element == str {
			return true
		}
	}
	return false
}


<|start_filename|>utils/pythonutils/pipenvutils.go<|end_filename|>
package pythonutils

import (
	"encoding/json"
	"github.com/jfrog/build-info-go/utils"
)

// Executes pipenv graph.
// Returns a dependency map of all the installed pipenv packages in the current environment and also another list of the top level dependencies
// 'dependenciesGraph' - map between all parent modules and their child dependencies
// 'topLevelPackagesList' - list of all top level dependencies ( root dependencies only)
func getPipenvDependencies(srcPath string) (dependenciesGraph map[string][]string, topLevelDependencies []string, err error) {
	// Run pipenv graph
	pipenvGraphCmd := utils.NewCommand("pipenv", "graph", []string{"--json"})
	pipenvGraphCmd.Dir = srcPath
	output, err := pipenvGraphCmd.RunWithOutput()
	if err != nil {
		return
	}
	// Parse into array.
	packages := make([]pythonDependencyPackage, 0)
	err = json.Unmarshal(output, &packages)
	if err != nil {
		return
	}
	dependenciesGraph, topLevelDependencies, err = parseDependenciesToGraph(packages)
	return
}


<|start_filename|>buildinfoschema_test.go<|end_filename|>
package main

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/jfrog/build-info-go/build/testdata"
	buildutils "github.com/jfrog/build-info-go/build/utils"
	"github.com/jfrog/build-info-go/utils"

	"github.com/stretchr/testify/assert"
	"github.com/xeipuuv/gojsonschema"
)

func TestGoSchema(t *testing.T) {
	validateBuildInfoSchema(t, "go", filepath.Join("golang", "project"), func() {})
}

func TestMavenSchema(t *testing.T) {
	validateBuildInfoSchema(t, "mvn", filepath.Join("maven", "project"), func() {})
}

func TestNpmSchema(t *testing.T) {
	validateBuildInfoSchema(t, "npm", filepath.Join("npm", "project1", "npmv8"), func() {
		_, _, err := buildutils.RunNpmCmd("npm", "", buildutils.Install, []string{}, &utils.NullLog{})
		assert.NoError(t, err)
	})
}

func TestYarnSchema(t *testing.T) {
	validateBuildInfoSchema(t, "yarn", filepath.Join("yarn", "project"), func() {})
}

// Validate a build info schema for the input project.
// t              - The testing object
// commandName    - Command to run such as npm, yarn, mvn, and go
// pathInTestData - The path of the test project in testdata dir
// install        - Install the project, if needed
func validateBuildInfoSchema(t *testing.T, commandName, pathInTestData string, install func()) {
	// Load build-info schema
	schema, err := ioutil.ReadFile("buildinfo-schema.json")
	assert.NoError(t, err)
	schemaLoader := gojsonschema.NewBytesLoader(schema)

	// Prepare test project
	cleanUp := prepareProject(t, pathInTestData, install)

	// Generate build-info
	buildInfoContent := generateBuildInfo(t, commandName)
	documentLoader := gojsonschema.NewBytesLoader(buildInfoContent)

	// Validate build-info schema
	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
	assert.NoError(t, err)
	assert.True(t, result.Valid(), result.Errors())

	// Clean up
	cleanUp()
}

// Copy project from testdata to a temp directory and change dir to the temp dir.
// t              - The testing object
// pathInTestdata - The path of the test project in to the testdata dir
// install -      - Installs the project
// Returns a cleanup callback
func prepareProject(t *testing.T, pathInTestdata string, install func()) func() {
	wd, err := os.Getwd()
	assert.NoError(t, err)
	tempDir, cleanup := testdata.CreateTestProject(t, filepath.Join("build", "testdata", pathInTestdata))
	assert.NoError(t, os.Chdir(tempDir))
	install()

	return func() {
		assert.NoError(t, os.Chdir(wd))
		cleanup()
	}
}

// Generate build-info by running "bi <commandName>" in the current working directory
// t              - The testing object
// commandName    - Command to run such as npm, yarn, mvn, and go
// Returns the buildinfo content
func generateBuildInfo(t *testing.T, commandName string) []byte {
	// Save old state
	oldArgs := os.Args
	oldStdout := os.Stdout

	// Create a commandOutput file
	commandOutput, err := os.CreateTemp("", "output")
	assert.NoError(t, err)

	defer func() {
		os.Stdout = oldStdout
		os.Args = oldArgs
		assert.NoError(t, commandOutput.Close())
		assert.NoError(t, os.Remove(commandOutput.Name()))
	}()

	// Execute command with output redirection to a temp file
	os.Stdout = commandOutput
	os.Args = []string{"bi", commandName}
	main()

	// Read output
	content, err := ioutil.ReadFile(commandOutput.Name())
	assert.NoError(t, err)
	assert.NotEmpty(t, content)
	return content
}


<|start_filename|>build/python_test.go<|end_filename|>
package build

import (
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils/pythonutils"

	testdatautils "github.com/jfrog/build-info-go/build/testdata"
	"github.com/stretchr/testify/assert"
)

func TestGenerateBuildInfoForPython(t *testing.T) {
	allTests := []struct {
		name                string
		pythonTool          pythonutils.PythonTool
		cmdArgs             []string
		moduleName          string
		expectedResultsJson string
	}{
		{"pip-with-module", pythonutils.Pip, []string{".", "--no-cache-dir", "--force-reinstall"}, "testModuleName:1.0.0", "expected_pip_buildinfo_with_module_name.json"},
		{"pip-without-module", pythonutils.Pip, []string{".", "--no-cache-dir", "--force-reinstall"}, "", "expected_pip_buildinfo_without_module_name.json"},

		{"pipenv-with-module", pythonutils.Pipenv, []string{}, "testModuleName:1.0.0", "expected_pipenv_buildinfo_with_module_name.json"},
		{"pipenv-without-module", pythonutils.Pipenv, []string{}, "", "expected_pipenv_buildinfo_without_module_name.json"},
	}
	// Run test cases.
	for _, test := range allTests {
		t.Run(test.name, func(t *testing.T) {
			testGenerateBuildInfoForPython(t, test.pythonTool, test.cmdArgs, test.moduleName, test.expectedResultsJson)
		})
	}
}

func testGenerateBuildInfoForPython(t *testing.T, pythonTool pythonutils.PythonTool, cmdArgs []string, moduleName, expectedResultsJson string) {
	service := NewBuildInfoService()
	pythonBuild, err := service.GetOrCreateBuild("build-info-go-test-"+string(pythonTool), strconv.FormatInt(time.Now().Unix(), 10))
	assert.NoError(t, err)
	defer func() {
		assert.NoError(t, pythonBuild.Clean())
	}()
	// Create npm project.
	testdataDir, err := filepath.Abs(filepath.Join(".", "testdata"))
	assert.NoError(t, err)
	// Create python project
	projectPath := filepath.Join(testdataDir, "python", string(pythonTool))
	tmpProjectPath, cleanup := testdatautils.CreateTestProject(t, projectPath)
	defer cleanup()

	// Install dependencies in the pip project.
	pythonModule, err := pythonBuild.AddPythonModule(tmpProjectPath, pythonTool)
	assert.NoError(t, err)
	pythonModule.SetName(moduleName)
	assert.NoError(t, pythonModule.RunInstallAndCollectDependencies(cmdArgs))
	buildInfo, err := pythonBuild.ToBuildInfo()
	assert.NoError(t, err)
	// Verify results.
	expectedBuildInfoJson := filepath.Join(projectPath, expectedResultsJson)
	expectedBuildInfo := testdatautils.GetBuildInfo(t, expectedBuildInfoJson)
	match, err := entities.IsEqualModuleSlices(buildInfo.Modules, expectedBuildInfo.Modules)
	assert.NoError(t, err)
	if !match {
		testdatautils.PrintBuildInfoMismatch(t, expectedBuildInfo.Modules, buildInfo.Modules)
	}
}


<|start_filename|>build/python.go<|end_filename|>
package build

import (
	"fmt"
	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
	"github.com/jfrog/build-info-go/utils/pythonutils"
	gofrogcmd "github.com/jfrog/gofrog/io"
	"os"
	"regexp"
	"strings"
)

type PythonModule struct {
	containingBuild            *Build
	tool                       pythonutils.PythonTool
	name                       string
	srcPath                    string
	localDependenciesPath      string
	updateDepsChecksumInfoFunc func(dependenciesMap map[string]entities.Dependency, srcPath string) error
}

func newPythonModule(srcPath string, tool pythonutils.PythonTool, containingBuild *Build) (*PythonModule, error) {
	var err error
	if srcPath == "" {
		srcPath, err = os.Getwd()
		if err != nil {
			return nil, err
		}
	}
	return &PythonModule{srcPath: srcPath, containingBuild: containingBuild, tool: tool}, nil
}

func (pm *PythonModule) RunInstallAndCollectDependencies(commandArgs []string) error {
	dependenciesMap, err := pm.InstallWithLogParsing(commandArgs)
	if err != nil {
		return err
	}
	dependenciesGraph, topLevelPackagesList, err := pythonutils.GetPythonDependencies(pm.tool, pm.srcPath, pm.localDependenciesPath)
	if err != nil {
		return fmt.Errorf("failed while attempting to get %s dependencies graph: %s", string(pm.tool), err.Error())
	}
	// Get package-name.
	packageName, pkgNameErr := pythonutils.GetPackageNameFromSetuppy(pm.srcPath)
	if pkgNameErr != nil {
		pm.containingBuild.logger.Debug("Couldn't retrieve the package name from Setup.py. Reason: ", pkgNameErr.Error())
	}
	// If module-name was set by the command, don't change it.
	if pm.name == "" {
		// If the package name is unknown, set the module name to be the build name.
		pm.name = packageName
		if pm.name == "" {
			pm.name = pm.containingBuild.buildName
			pm.containingBuild.logger.Debug(fmt.Sprintf("Using build name: %s as module name.", pm.name))
		}
	}
	if pm.updateDepsChecksumInfoFunc != nil {
		err = pm.updateDepsChecksumInfoFunc(dependenciesMap, pm.srcPath)
		if err != nil {
			return err
		}
	}
	pythonutils.UpdateDepsIdsAndRequestedBy(dependenciesMap, dependenciesGraph, topLevelPackagesList, packageName, pm.name)
	buildInfoModule := entities.Module{Id: pm.name, Type: entities.Python, Dependencies: dependenciesMapToList(dependenciesMap)}
	buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}

	return pm.containingBuild.SaveBuildInfo(buildInfo)
}

// Run install command while parsing the logs for downloaded packages.
// Populates 'downloadedDependencies' with downloaded package-name and its actual downloaded file (wheel/egg/zip...).
func (pm *PythonModule) InstallWithLogParsing(commandArgs []string) (map[string]entities.Dependency, error) {
	log := pm.containingBuild.logger
	if pm.tool == pythonutils.Pipenv {
		// Add verbosity flag to pipenv commands to collect necessary data
		commandArgs = append(commandArgs, "-v")
	}
	installCmd := utils.NewCommand(string(pm.tool), "install", commandArgs)
	installCmd.Dir = pm.srcPath

	dependenciesMap := map[string]entities.Dependency{}

	// Create regular expressions for log parsing.
	collectingRegexp, err := regexp.Compile(`^Collecting\s(\w[\w-.]+)`)
	if err != nil {
		return nil, err
	}
	downloadingRegexp, err := regexp.Compile(`^\s*Downloading\s([^\s]*)\s\(`)
	if err != nil {
		return nil, err
	}
	usingCachedRegexp, err := regexp.Compile(`^\s*Using\scached\s([\S]+)\s\(`)
	if err != nil {
		return nil, err
	}
	alreadySatisfiedRegexp, err := regexp.Compile(`^Requirement\salready\ssatisfied:\s(\w[\w-.]+)`)
	if err != nil {
		return nil, err
	}

	var packageName string
	expectingPackageFilePath := false

	// Extract downloaded package name.
	dependencyNameParser := gofrogcmd.CmdOutputPattern{
		RegExp: collectingRegexp,
		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
			// If this pattern matched a second time before downloaded-file-name was found, prompt a message.
			if expectingPackageFilePath {
				// This may occur when a package-installation file is saved in pip-cache-dir, thus not being downloaded during the installation.
				// Re-running pip-install with 'no-cache-dir' fixes this issue.
				log.Debug(fmt.Sprintf("Could not resolve download path for package: %s, continuing...", packageName))

				// Save package with empty file path.
				dependenciesMap[strings.ToLower(packageName)] = entities.Dependency{Id: ""}
			}

			// Check for out of bound results.
			if len(pattern.MatchedResults)-1 < 0 {
				log.Debug(fmt.Sprintf("Failed extracting package name from line: %s", pattern.Line))
				return pattern.Line, nil
			}

			// Save dependency information.
			expectingPackageFilePath = true
			packageName = pattern.MatchedResults[1]

			return pattern.Line, nil
		},
	}

	// Extract downloaded file, stored in Artifactory.
	downloadedFileParser := gofrogcmd.CmdOutputPattern{
		RegExp: downloadingRegexp,
		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
			// Check for out of bound results.
			if len(pattern.MatchedResults)-1 < 0 {
				log.Debug(fmt.Sprintf("Failed extracting download path from line: %s", pattern.Line))
				return pattern.Line, nil
			}

			// If this pattern matched before package-name was found, do not collect this path.
			if !expectingPackageFilePath {
				log.Debug(fmt.Sprintf("Could not resolve package name for download path: %s , continuing...", packageName))
				return pattern.Line, nil
			}

			// Save dependency information.
			filePath := pattern.MatchedResults[1]
			lastSlashIndex := strings.LastIndex(filePath, "/")
			var fileName string
			if lastSlashIndex == -1 {
				fileName = filePath
			} else {
				fileName = filePath[lastSlashIndex+1:]
			}
			dependenciesMap[strings.ToLower(packageName)] = entities.Dependency{Id: fileName}
			expectingPackageFilePath = false

			log.Debug(fmt.Sprintf("Found package: %s installed with: %s", packageName, fileName))
			return pattern.Line, nil
		},
	}

	cachedFileParser := gofrogcmd.CmdOutputPattern{
		RegExp:   usingCachedRegexp,
		ExecFunc: downloadedFileParser.ExecFunc,
	}

	// Extract already installed packages names.
	installedPackagesParser := gofrogcmd.CmdOutputPattern{
		RegExp: alreadySatisfiedRegexp,
		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
			// Check for out of bound results.
			if len(pattern.MatchedResults)-1 < 0 {
				log.Debug(fmt.Sprintf("Failed extracting package name from line: %s", pattern.Line))
				return pattern.Line, nil
			}

			// Save dependency with empty file name.
			dependenciesMap[strings.ToLower(pattern.MatchedResults[1])] = entities.Dependency{Id: ""}
			log.Debug(fmt.Sprintf("Found package: %s already installed", pattern.MatchedResults[1]))
			return pattern.Line, nil
		},
	}

	// Execute command.
	var errorOut string
	_, errorOut, _, err = gofrogcmd.RunCmdWithOutputParser(installCmd, true, &dependencyNameParser, &downloadedFileParser, &cachedFileParser, &installedPackagesParser)
	if err != nil {
		return nil, fmt.Errorf("failed running %s command with error: '%s - %s'", string(pm.tool), err.Error(), errorOut)
	}
	return dependenciesMap, nil
}

func (pm *PythonModule) SetName(name string) {
	pm.name = name
}

func (pm *PythonModule) SetLocalDependenciesPath(localDependenciesPath string) {
	pm.localDependenciesPath = localDependenciesPath
}

func (pm *PythonModule) SetUpdateDepsChecksumInfoFunc(updateDepsChecksumInfoFunc func(dependenciesMap map[string]entities.Dependency, srcPath string) error) {
	pm.updateDepsChecksumInfoFunc = updateDepsChecksumInfoFunc
}


<|start_filename|>entities/buildinfo_test.go<|end_filename|>
package entities

import (
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestIsEqualModuleSlices(t *testing.T) {
	a := []Module{{
		Type: "docker",
		Id:   "manifest",
		Artifacts: []Artifact{{
			Name: "layer",
			Type: "",
			Path: "path/to/somewhere",
			Checksum: Checksum{
				Sha1: "1",
				Md5:  "2",
			},
		}},
		Dependencies: []Dependency{{
			Id:   "alpine",
			Type: "docker",
			Checksum: Checksum{
				Sha1: "3",
				Md5:  "4",
			},
		}},
	}}
	b := []Module{{
		Type: "docker",
		Id:   "manifest",
		Artifacts: []Artifact{{
			Name: "layer",
			Type: "",
			Path: "path/to/somewhere",
			Checksum: Checksum{
				Sha1: "1",
				Md5:  "2",
			},
		}},
		Dependencies: []Dependency{{
			Id:   "alpine",
			Type: "docker",
			Checksum: Checksum{
				Sha1: "3",
				Md5:  "4",
			},
		}},
	}}
	match, err := IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.True(t, match)

	b[0].Type = "other"
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.False(t, match)

	b[0].Type = "docker"
	b[0].Id = "other"
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.False(t, match)

	b[0].Id = "manifest"
	b[0].Artifacts[0].Name = "other"
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.False(t, match)

	b[0].Artifacts[0].Name = "layer"
	newDependency := Dependency{
		Id:   "alpine",
		Type: "docker",
		Checksum: Checksum{
			Sha1: "3",
			Md5:  "4",
		},
	}
	b[0].Dependencies = append(b[0].Dependencies, newDependency)
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.False(t, match)
	a[0].Dependencies = append(a[0].Dependencies, newDependency)
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.True(t, match)

	newArtifact := Artifact{
		Name:     "a",
		Type:     "s",
		Path:     "s",
		Checksum: Checksum{},
	}
	a[0].Artifacts = append(a[0].Artifacts, newArtifact)
	match, err = IsEqualModuleSlices(a, b)
	assert.NoError(t, err)
	assert.False(t, match)
}

func TestIsEqualModuleSlicesRegex(t *testing.T) {
	actual := []Module{{
		Type: "docker",
		Id:   "manifest",
		Artifacts: []Artifact{{
			Name: "sha256__7d3b33ae048d1",
			Type: "json",
			Path: "image-name-multiarch-image/sha256:a56b64f",
			Checksum: Checksum{
				Sha1: "1",
				Md5:  "2",
			},
		}},
		Dependencies: []Dependency{{
			Id:   "sha256__7d3b33ae048d1",
			Type: "docker",
			Checksum: Checksum{
				Sha1: "3",
				Md5:  "4",
			},
		}},
	}}
	expected := []Module{{
		Type: "docker",
		Id:   "manifest",
		Artifacts: []Artifact{{
			Name: "sha256__*",
			Type: "json",
			Path: "image-name-multiarch-image*",
			Checksum: Checksum{
				Sha1: ".+",
				Md5:  ".+",
			},
		}},
		Dependencies: []Dependency{{
			Id:   "sha256__*",
			Type: "docker",
			Checksum: Checksum{
				Sha1: ".+",
				Md5:  ".+",
			},
		}},
	}}
	match, err := IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.True(t, match)
	// Validate dependencies
	actual[0].Dependencies[0].Sha1 = ""
	match, err = IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.False(t, match)
	actual[0].Dependencies[0].Sha1 = "123"

	actual[0].Dependencies[0].Id = ""
	match, err = IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.False(t, match)
	actual[0].Dependencies[0].Id = "sha256__7d3b33ae048d1"

	// Validate artifact
	actual[0].Artifacts[0].Sha1 = ""
	match, err = IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.False(t, match)
	actual[0].Artifacts[0].Sha1 = "123"

	actual[0].Artifacts[0].Path = "a"
	match, err = IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.False(t, match)
	actual[0].Artifacts[0].Sha1 = "image-name-multiarch-image/sha256:a56b64f"

	actual[0].Artifacts[0].Name = ""
	match, err = IsEqualModuleSlices(actual, expected)
	assert.NoError(t, err)
	assert.False(t, match)
}

func TestMergeDependenciesLists(t *testing.T) {
	dependenciesToAdd := []Dependency{
		{Id: "test-dep1", Type: "tst", Scopes: []string{"a", "b"}, RequestedBy: [][]string{{"a", "b"}, {"b", "a"}}},
		{Id: "test-dep2", Type: "tst", Scopes: []string{"a"}, RequestedBy: [][]string{{"a", "b"}}, Checksum: Checksum{Sha1: "123"}},
		{Id: "test-dep3", Type: "tst"},
		{Id: "test-dep4", Type: "tst"},
	}
	intoDependencies := []Dependency{
		{Id: "test-dep1", Type: "tst", Scopes: []string{"a"}, RequestedBy: [][]string{{"b", "a"}}},
		{Id: "test-dep2", Type: "tst", Scopes: []string{"b"}, RequestedBy: [][]string{{"a", "c"}}, Checksum: Checksum{Sha1: "123"}},
		{Id: "test-dep3", Type: "tst", Scopes: []string{"a"}, RequestedBy: [][]string{{"a", "b"}}},
	}
	expectedMergedDependencies := []Dependency{
		{Id: "test-dep1", Type: "tst", Scopes: []string{"a", "b"}, RequestedBy: [][]string{{"b", "a"}, {"a", "b"}}},
		{Id: "test-dep2", Type: "tst", Scopes: []string{"b"}, RequestedBy: [][]string{{"a", "c"}}, Checksum: Checksum{Sha1: "123"}},
		{Id: "test-dep3", Type: "tst", Scopes: []string{"a"}, RequestedBy: [][]string{{"a", "b"}}},
		{Id: "test-dep4", Type: "tst"},
	}
	mergeDependenciesLists(&dependenciesToAdd, &intoDependencies)
	reflect.DeepEqual(expectedMergedDependencies, intoDependencies)
}

func TestAppend(t *testing.T) {
	artifactA := Artifact{Name: "artifact-a", Checksum: Checksum{Sha1: "artifact-a-sha"}}
	artifactB := Artifact{Name: "artifact-b", Checksum: Checksum{Sha1: "artifact-b-sha"}}
	artifactC := Artifact{Name: "artifact-c", Checksum: Checksum{Sha1: "artifact-c-sha"}}

	dependencyA := Dependency{Id: "dependency-a", Checksum: Checksum{Sha1: "dependency-a-sha"}}
	dependencyB := Dependency{Id: "dependency-b", Checksum: Checksum{Sha1: "dependency-b-sha"}}
	dependencyC := Dependency{Id: "dependency-c", Checksum: Checksum{Sha1: "dependency-c-sha"}}

	buildInfo1 := BuildInfo{
		Modules: []Module{{
			Id:           "module-id",
			Artifacts:    []Artifact{artifactA, artifactB},
			Dependencies: []Dependency{dependencyA, dependencyB},
		}},
	}

	buildInfo2 := BuildInfo{
		Modules: []Module{{
			Id:           "module-id",
			Artifacts:    []Artifact{artifactA, artifactC},
			Dependencies: []Dependency{dependencyA, dependencyC},
		}},
	}

	expected := BuildInfo{
		Modules: []Module{{
			Id:           "module-id",
			Artifacts:    []Artifact{artifactA, artifactB, artifactC},
			Dependencies: []Dependency{dependencyA, dependencyB, dependencyC},
		}},
	}

	buildInfo1.Append(&buildInfo2)
	results, err := IsEqualModuleSlices(expected.Modules, buildInfo1.Modules)
	assert.NoError(t, err)
	assert.True(t, results)
}


<|start_filename|>build/utils/npm.go<|end_filename|>
package utils

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/buger/jsonparser"
	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
	"github.com/jfrog/gofrog/version"
)

// CalculateNpmDependenciesList gets an npm project's dependencies.
func CalculateNpmDependenciesList(executablePath, srcPath, moduleId string, npmArgs []string, calculateChecksums bool, log utils.Log) ([]entities.Dependency, error) {
	if log == nil {
		log = &utils.NullLog{}
	}
	// Calculate npm dependency tree using 'npm ls...'.
	dependenciesMap, err := CalculateDependenciesMap(executablePath, srcPath, moduleId, npmArgs, log)
	if err != nil {
		return nil, err
	}
	var cacache *cacache
	if calculateChecksums {
		// Get local npm cache.
		cacheLocation, err := GetNpmConfigCache(srcPath, executablePath, npmArgs, log)
		if err != nil {
			return nil, err
		}
		cacache = NewNpmCacache(cacheLocation)
	}
	var dependenciesList []entities.Dependency
	var missingPeerDeps, missingBundledDeps, missingOptionalDeps, otherMissingDeps []string
	for _, dep := range dependenciesMap {
		if dep.npmLsDependency.Integrity == "" && dep.npmLsDependency.InBundle {
			missingBundledDeps = append(missingBundledDeps, dep.Id)
			continue
		}
		if dep.npmLsDependency.Integrity == "" && dep.PeerMissing != nil {
			missingPeerDeps = append(missingPeerDeps, dep.Id)
			continue
		}
		if calculateChecksums {
			dep.Md5, dep.Sha1, dep.Sha256, err = calculateChecksum(cacache, dep.Name, dep.Version, dep.Integrity, log)
			if err != nil {
				if dep.Optional {
					missingOptionalDeps = append(missingOptionalDeps, dep.Id)
					continue
				}
				// Here, we don't know where is the tarball (or if it is actually exists in the filesystem) so we can't calculate the dependency checksum.
				// This case happends when the package-lock.json with property '"lockfileVersion": 1,' gets updated to version '"lockfileVersion": 2,' (from npm v6 to npm v7/v8).
				// Seems like the compatibility upgrades may result in dependencies losing their integrity.
				// We use the integrity to get's the dependencies tarball
				otherMissingDeps = append(otherMissingDeps, dep.Id)
				log.Debug("couldn't calculate checksum for " + dep.Id + ". Error: '" + err.Error() + "'.")
				continue
			}
		}

		dependenciesList = append(dependenciesList, dep.Dependency)
	}
	if len(missingPeerDeps) > 0 {
		printMissingDependenciesWarning("peerDependency", missingPeerDeps, log)
	}
	if len(missingBundledDeps) > 0 {
		printMissingDependenciesWarning("bundleDependencies", missingBundledDeps, log)
	}
	if len(missingOptionalDeps) > 0 {
		printMissingDependenciesWarning("optionalDependencies", missingOptionalDeps, log)
	}
	if len(otherMissingDeps) > 0 {
		log.Warn("The following dependencies will not be included in the build-info, because they are missing in the npm cache: '" + strings.Join(otherMissingDeps, ",") + "'.\nHint: Try to delete 'node_models' and/or 'package-lock.json'.")
	}
	return dependenciesList, nil
}

type dependencyInfo struct {
	entities.Dependency
	*npmLsDependency
}

// Run 'npm list ...' command and parse the returned result to create a dependencies map of.
// The dependencies map looks like name:version -> entities.Dependency
func CalculateDependenciesMap(executablePath, srcPath, moduleId string, npmArgs []string, log utils.Log) (map[string]*dependencyInfo, error) {
	dependenciesMap := make(map[string]*dependencyInfo)

	// These arguments must be added at the end of the command, to override their other values (if existed in nm.npmArgs)
	npmArgs = append(npmArgs, "--json", "--all", "--long")
	data, errData, err := RunNpmCmd(executablePath, srcPath, Ls, npmArgs, log)
	// Some warnings and messages of npm are printed to stderr. They don't cause the command to fail, but we'd want to show them to the user.
	if err != nil {
		found, isDirExistsErr := utils.IsDirExists(filepath.Join(srcPath, "node_modules"), false)
		if isDirExistsErr != nil {
			return nil, isDirExistsErr
		}
		if !found {
			return nil, errors.New("node_modules isn't found in '" + srcPath + "'. Hint: Restore node_modules folder by running npm install or npm ci.")
		}
		log.Warn("npm list command failed with error:", err.Error())
	}
	if len(errData) > 0 {
		log.Warn("Some errors occurred while collecting dependencies info:\n" + string(errData))
	}
	npmVersion, err := GetNpmVersion(executablePath, log)
	if err != nil {
		return nil, err
	}
	parseFunc := parseNpmLsDependencyFunc(npmVersion)

	// Parse the dependencies json object.
	return dependenciesMap, jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) (err error) {
		if string(key) == "dependencies" {
			err = parseDependencies(value, []string{moduleId}, dependenciesMap, parseFunc, log)
		}
		return err
	})
}

func GetNpmVersion(executablePath string, log utils.Log) (*version.Version, error) {
	versionData, _, err := RunNpmCmd(executablePath, "", Version, nil, log)
	if err != nil {
		return nil, err
	}
	return version.NewVersion(string(versionData)), nil
}

// npm >=7 ls results for a single dependency
type npmLsDependency struct {
	Name      string
	Version   string
	Integrity string
	InBundle  bool
	Dev       bool
	Optional  bool
	// Missing peer dependency in npm version 7/8
	Missing bool
	// Problems with missing peer dependency in npm version 7/8
	Problems []string
	// Missing  peer dependency in npm version 6
	// Bound to 'legacyNpmLsDependency' struct
	PeerMissing interface{}
}

// npm 6 ls results for a single dependency
type legacyNpmLsDependency struct {
	Name          string
	Version       string
	Missing       bool
	Integrity     string `json:"_integrity,omitempty"`
	InBundle      bool   `json:"_inBundle,omitempty"`
	Dev           bool   `json:"_development,omitempty"`
	InnerOptional bool   `json:"_optional,omitempty"`
	Optional      bool
	PeerMissing   interface{}
}

func (lnld *legacyNpmLsDependency) optional() bool {
	if lnld.Optional {
		return true
	}
	return lnld.InnerOptional
}

func (lnld *legacyNpmLsDependency) toNpmLsDependency() *npmLsDependency {
	return &npmLsDependency{
		Name:        lnld.Name,
		Version:     lnld.Version,
		Integrity:   lnld.Integrity,
		InBundle:    lnld.InBundle,
		Dev:         lnld.Dev,
		Optional:    lnld.optional(),
		Missing:     lnld.Missing,
		PeerMissing: lnld.PeerMissing,
	}
}

// Return name:version of a dependency
func (nld *npmLsDependency) id() string {
	return nld.Name + ":" + nld.Version
}

func (nld *npmLsDependency) getScopes() (scopes []string) {
	if nld.Dev {
		scopes = append(scopes, "dev")
	} else {
		scopes = append(scopes, "prod")
	}
	if strings.HasPrefix(nld.Name, "@") {
		splitValues := strings.Split(nld.Name, "/")
		if len(splitValues) > 2 {
			scopes = append(scopes, splitValues[0])
		}
	}
	return
}

// Parses npm dependencies recursively and adds the collected dependencies to the given dependencies map.
func parseDependencies(data []byte, pathToRoot []string, dependencies map[string]*dependencyInfo, parseFunc func(data []byte) (*npmLsDependency, error), log utils.Log) error {
	return jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
		if string(value) == "{}" {
			// Skip missing optional dependency.
			log.Debug(fmt.Sprintf("%s is missing. This may be the result of an optional dependency.", key))
			return nil
		}
		npmLsDependency, err := parseFunc(value)
		if err != nil {
			return err
		}
		if npmLsDependency.Version == "" {
			if npmLsDependency.Missing || npmLsDependency.Problems != nil {
				// Skip missing peer dependency.
				log.Debug(fmt.Sprintf("%s is missing, this may be the result of an peer dependency.", key))
				return nil
			}
			return errors.New("failed to parse '" + string(value) + "' from npm ls output.")
		}
		appendDependency(dependencies, npmLsDependency, pathToRoot, log)
		transitive, _, _, err := jsonparser.Get(value, "dependencies")
		if err != nil && err.Error() != "Key path not found" {
			return err
		}
		if len(transitive) > 0 {
			if err := parseDependencies(transitive, append([]string{npmLsDependency.id()}, pathToRoot...), dependencies, parseFunc, log); err != nil {
				return err
			}
		}
		return nil
	})
}

func parseNpmLsDependencyFunc(npmVersion *version.Version) func(data []byte) (*npmLsDependency, error) {
	// If npm older than v7, use legacy struct for npm ls output.
	if npmVersion.Compare("7.0.0") > 0 {
		return legacyNpmLsDependencyParser
	}
	return npmLsDependencyParser
}

func legacyNpmLsDependencyParser(data []byte) (*npmLsDependency, error) {
	legacyNpmLsDependency := new(legacyNpmLsDependency)
	err := json.Unmarshal(data, &legacyNpmLsDependency)
	if err != nil {
		return nil, err
	}
	return legacyNpmLsDependency.toNpmLsDependency(), nil
}

func npmLsDependencyParser(data []byte) (*npmLsDependency, error) {
	npmLsDependency := new(npmLsDependency)
	return npmLsDependency, json.Unmarshal(data, &npmLsDependency)
}

func appendDependency(dependencies map[string]*dependencyInfo, dep *npmLsDependency, pathToRoot []string, log utils.Log) {
	depId := dep.id()
	scopes := dep.getScopes()
	if dependencies[depId] == nil {
		dependency := &dependencyInfo{
			Dependency:      entities.Dependency{Id: depId},
			npmLsDependency: dep,
		}

		dependencies[depId] = dependency
	}
	if dependencies[depId].Integrity == "" {
		dependencies[depId].Integrity = dep.Integrity
	}
	dependencies[depId].Scopes = appendScopes(dependencies[depId].Scopes, scopes)
	dependencies[depId].RequestedBy = append(dependencies[depId].RequestedBy, pathToRoot)
}

// Lookup for a dependency's tarball in npm cache, and calculate checksum.
func calculateChecksum(cacache *cacache, name, version, integrity string, log utils.Log) (md5 string, sha1 string, sha256 string, err error) {
	if integrity == "" {
		var info *cacacheInfo
		info, err = cacache.GetInfo(name + "@" + version)
		if err != nil {
			return
		}
		integrity = info.Integrity
	}
	var path string
	path, err = cacache.GetTarball(integrity)
	if err != nil {
		return
	}
	return utils.GetFileChecksums(path)
}

// Merge two scopes and remove duplicates.
func appendScopes(oldScopes []string, newScopes []string) []string {
	contained := make(map[string]bool)
	allScopes := []string{}
	for _, scope := range append(oldScopes, newScopes...) {
		if scope == "" {
			continue
		}
		if !contained[scope] {
			allScopes = append(allScopes, scope)
		}
		contained[scope] = true
	}
	return allScopes
}

type NpmCmd int

const (
	Ls NpmCmd = iota
	Config
	Install
	Ci
	Pack
	Version
)

func (nc NpmCmd) String() string {
	return [...]string{"ls", "config", "install", "ci", "pack", "-version"}[nc]
}

func RunNpmCmd(executablePath, srcPath string, npmCmd NpmCmd, npmArgs []string, log utils.Log) (stdResult, errResult []byte, err error) {
	log.Debug("Running npm " + npmCmd.String() + " command.")
	cmdArgs := []string{npmCmd.String()}
	tmpArgs := make([]string, 0)
	for i := 0; i < len(npmArgs); i++ {
		if strings.TrimSpace(npmArgs[i]) != "" {
			tmpArgs = append(tmpArgs, npmArgs[i])
		}
	}
	cmdArgs = append(cmdArgs, tmpArgs...)

	command := exec.Command(executablePath, cmdArgs...)
	command.Dir = srcPath
	outBuffer := bytes.NewBuffer([]byte{})
	command.Stdout = outBuffer
	errBuffer := bytes.NewBuffer([]byte{})
	command.Stderr = errBuffer
	err = command.Run()
	errResult = errBuffer.Bytes()
	stdResult = outBuffer.Bytes()
	if err != nil {
		err = errors.New("error while running the command :'" + executablePath + " " + strings.Join(cmdArgs, " ") + "'\nError output is:\n" + string(errResult) + "'\nCommand error: is:\n" + string(err.Error()))
		return
	}
	log.Debug("npm " + npmCmd.String() + " standard output is:\n" + string(stdResult))
	return
}

func GetNpmVersionAndExecPath(log utils.Log) (*version.Version, string, error) {
	if log == nil {
		log = &utils.NullLog{}
	}
	npmExecPath, err := exec.LookPath("npm")
	if err != nil {
		return nil, "", err
	}

	if npmExecPath == "" {
		return nil, "", errors.New("could not find the 'npm' executable in the system PATH")
	}

	log.Debug("Using npm executable:", npmExecPath)

	versionData, _, err := RunNpmCmd(npmExecPath, "", Version, nil, log)
	if err != nil {
		return nil, "", err
	}
	log.Debug("Using npm version:", string(versionData))
	return version.NewVersion(string(versionData)), npmExecPath, nil
}

type PackageInfo struct {
	Name    string `json:"name,omitempty"`
	Version string `json:"version,omitempty"`
	Scope   string
}

func ReadPackageInfoFromPackageJson(packageJsonDirectory string, npmVersion *version.Version) (*PackageInfo, error) {
	packageJson, err := ioutil.ReadFile(filepath.Join(packageJsonDirectory, "package.json"))
	if err != nil {
		return nil, err
	}
	return ReadPackageInfo(packageJson, npmVersion)
}

func ReadPackageInfo(data []byte, npmVersion *version.Version) (*PackageInfo, error) {
	parsedResult := new(PackageInfo)
	if err := json.Unmarshal(data, parsedResult); err != nil {
		return nil, err
	}
	// If npm older than v7, remove prefixes.
	if npmVersion != nil && npmVersion.Compare("7.0.0") > 0 {
		removeVersionPrefixes(parsedResult)
	}
	splitScopeFromName(parsedResult)
	return parsedResult, nil
}

func (pi *PackageInfo) BuildInfoModuleId() string {
	nameBase := fmt.Sprintf("%s:%s", pi.Name, pi.Version)
	if pi.Scope == "" {
		return nameBase
	}
	return fmt.Sprintf("%s:%s", strings.TrimPrefix(pi.Scope, "@"), nameBase)
}

func (pi *PackageInfo) GetDeployPath() string {
	fileName := fmt.Sprintf("%s-%s.tgz", pi.Name, pi.Version)
	if pi.Scope == "" {
		return fmt.Sprintf("%s/-/%s", pi.Name, fileName)
	}
	return fmt.Sprintf("%s/%s/-/%s", pi.Scope, pi.Name, fileName)
}

func (pi *PackageInfo) FullName() string {
	if pi.Scope == "" {
		return pi.Name
	}
	return fmt.Sprintf("%s/%s", pi.Scope, pi.Name)
}

func splitScopeFromName(packageInfo *PackageInfo) {
	if strings.HasPrefix(packageInfo.Name, "@") && strings.Contains(packageInfo.Name, "/") {
		splitValues := strings.Split(packageInfo.Name, "/")
		packageInfo.Scope = splitValues[0]
		packageInfo.Name = splitValues[1]
	}
}

// A leading "=" or "v" character is stripped off and ignored by npm.
func removeVersionPrefixes(packageInfo *PackageInfo) {
	packageInfo.Version = strings.TrimPrefix(packageInfo.Version, "v")
	packageInfo.Version = strings.TrimPrefix(packageInfo.Version, "=")
}

// Return the npm cache path.
// Default: Windows: %LocalAppData%\npm-cache, Posix: ~/.npm
func GetNpmConfigCache(srcPath, executablePath string, npmArgs []string, log utils.Log) (string, error) {
	npmArgs = append([]string{"get", "cache"}, npmArgs...)
	data, errData, err := RunNpmCmd(executablePath, srcPath, Config, append(npmArgs, "--json=false"), log)
	// Some warnings and messages of npm are printed to stderr. They don't cause the command to fail, but we'd want to show them to the user.
	if len(errData) > 0 {
		log.Warn("error while running the command :'" + executablePath + " " + strings.Join(npmArgs, " ") + ":\n" + string(errData))
	}
	if err != nil {
		return "", fmt.Errorf("'%s %s' npm config command failed with an error: %s", executablePath, strings.Join(npmArgs, " "), err.Error())
	}
	cachePath := filepath.Join(strings.Trim(string(data), "\n"), "_cacache")
	found, err := utils.IsDirExists(cachePath, true)
	if err != nil {
		return "", err
	}
	if !found {
		return "", errors.New("_cacache folder is not found in '" + cachePath + "'. Hint: Delete node_modules directory and run npm install or npm ci.")
	}
	return cachePath, nil
}

func printMissingDependenciesWarning(dependencyType string, dependencies []string, log utils.Log) {
	log.Debug("The following dependencies will not be included in the build-info, because the 'npm ls' command did not return their integrity.\nThe reason why the version wasn't returned may be because the package is a '" + dependencyType + "', which was not manually installed.\n It is therefore okay to skip this dependency: " + strings.Join(dependencies, ","))
}


<|start_filename|>utils/pythonutils/piputils.go<|end_filename|>
package pythonutils

import (
	"encoding/json"
	"errors"
	"github.com/jfrog/build-info-go/utils"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
)

// 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
func getPipDependencies(srcPath, dependenciesDirName string) (map[string][]string, []string, error) {
	pipDependencyMapScriptPath, err := getDepTreeScriptPath(dependenciesDirName)
	if err != nil {
		return nil, nil, err
	}
	pythonExecutable, windowsPyArg := getPythonExecutable()
	// Run pipdeptree script
	pipdeptreeCmd := utils.NewCommand(pythonExecutable, windowsPyArg, []string{pipDependencyMapScriptPath, "--json"})
	pipdeptreeCmd.Dir = srcPath
	output, err := pipdeptreeCmd.RunWithOutput()
	if err != nil {
		return nil, nil, err
	}
	// Parse into array.
	packages := make([]pythonDependencyPackage, 0)
	if err = json.Unmarshal(output, &packages); err != nil {
		return nil, nil, err
	}

	return parseDependenciesToGraph(packages)
}

// Return path to the dependency-tree script, If it doesn't exist, it creates the file.
func getDepTreeScriptPath(dependenciesDirName string) (string, error) {
	if dependenciesDirName == "" {
		home, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		dependenciesDirName = filepath.Join(home, dependenciesDirName, "pip")
	}
	depTreeScriptName := "pipdeptree.py"
	pipDependenciesPath := filepath.Join(dependenciesDirName, "pip", pipDepTreeVersion)
	depTreeScriptPath := filepath.Join(pipDependenciesPath, depTreeScriptName)
	err := writeScriptIfNeeded(pipDependenciesPath, depTreeScriptName)
	if err != nil {
		return "", err
	}
	return depTreeScriptPath, err
}

// Creates local python script on jfrog dependencies path folder if such not exists
func writeScriptIfNeeded(targetDirPath, scriptName string) error {
	scriptPath := filepath.Join(targetDirPath, scriptName)
	exists, err := utils.IsFileExists(scriptPath, false)
	if err != nil {
		return err
	}
	if !exists {
		err = os.MkdirAll(targetDirPath, os.ModeDir|os.ModePerm)
		if err != nil {
			return err
		}
		err = ioutil.WriteFile(scriptPath, pipDepTreeContent, os.ModePerm)
		if err != nil {
			return err
		}
	}
	return nil
}

func GetPackageNameFromSetuppy(srcPath string) (string, error) {
	filePath, err := getSetupPyFilePath(srcPath)
	if err != nil || filePath == "" {
		// Error was returned or setup.py does not exist in directory.
		return "", err
	}

	// Extract package name from setup.py.
	packageName, err := ExtractPackageNameFromSetupPy(filePath)
	if err != nil {
		// If setup.py egg_info command failed we use build name as module name and continue to pip-install execution
		return "", errors.New("couldn't determine module-name after running the 'egg_info' command: " + err.Error())
	}
	return packageName, nil
}

// Look for 'setup.py' file in current work dir.
// If found, return its absolute path.
func getSetupPyFilePath(srcPath string) (string, error) {
	filePath := filepath.Join(srcPath, "setup.py")
	// Check if setup.py exists.
	validPath, err := utils.IsFileExists(filePath, false)
	if err != nil {
		return "", err
	}
	if !validPath {
		return "", nil
	}

	return filePath, nil
}

// Get the project-name by running 'egg_info' command on setup.py and extracting it from 'PKG-INFO' file.
func ExtractPackageNameFromSetupPy(setuppyFilePath string) (string, error) {
	// Execute egg_info command and return PKG-INFO content.
	content, err := getEgginfoPkginfoContent(setuppyFilePath)
	if err != nil {
		return "", err
	}

	// Extract project name from file content.
	return getProjectIdFromFileContent(content)
}

// Run egg-info command on setup.py. The command generates metadata files.
// Return the content of the 'PKG-INFO' file.
func getEgginfoPkginfoContent(setuppyFilePath string) (output []byte, err error) {
	eggBase, err := utils.CreateTempDir()
	if err != nil {
		return nil, err
	}
	defer func() {
		e := utils.RemoveTempDir(eggBase)
		if err == nil {
			err = e
		}
	}()

	// Run python 'egg_info --egg-base <eggBase>' command.
	var args []string
	pythonExecutable, windowsPyArg := getPythonExecutable()
	if windowsPyArg != "" {
		args = append(args, windowsPyArg)
	}
	args = append(args, setuppyFilePath, "egg_info", "--egg-base", eggBase)
	if err != nil {
		return nil, err
	}
	if err = exec.Command(pythonExecutable, args...).Run(); err != nil {
		return nil, err
	}

	// Read PKG_INFO under <eggBase>/*.egg-info/PKG-INFO.
	return extractPackageNameFromEggBase(eggBase)
}

func getPythonExecutable() (string, string) {
	windowsPyArg := ""
	pythonExecutable, pathErr := exec.LookPath("python3")
	if pathErr != nil || pythonExecutable == "" {
		if runtime.GOOS == "windows" {
			// If the OS is Windows try using Py Launcher: 'py -3'
			pythonExecutable, pathErr = exec.LookPath("py")
			if pathErr != nil && pythonExecutable != "" {
				windowsPyArg = "-3"
			}
		}
		// Try using 'python' if 'python3'/'py' couldn't be found
		if pythonExecutable == "" {
			pythonExecutable = "python"
		}
	}
	return pythonExecutable, windowsPyArg

}

// Parse the output of 'python egg_info' command, in order to find the path of generated file 'PKG-INFO'.
func extractPackageNameFromEggBase(eggBase string) ([]byte, error) {
	files, err := os.ReadDir(eggBase)
	if err != nil {
		return nil, err
	}
	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".egg-info") {
			pkginfoPath := filepath.Join(eggBase, file.Name(), "PKG-INFO")
			// Read PKG-INFO file.
			pkginfoFileExists, err := utils.IsFileExists(pkginfoPath, false)
			if err != nil {
				return nil, err
			}
			if !pkginfoFileExists {
				return nil, errors.New("file 'PKG-INFO' couldn't be found in its designated location: " + pkginfoPath)
			}

			return os.ReadFile(pkginfoPath)
		}
	}

	return nil, errors.New("couldn't find pkg info files")
}

// Get package ID from PKG-INFO file content.
// If pattern of package name of version not found, return an error.
func getProjectIdFromFileContent(content []byte) (string, error) {
	// Create package-name regexp.
	packageNameRegexp, err := regexp.Compile(`(?m)^Name:\s(\w[\w-.]+)`)
	if err != nil {
		return "", err
	}

	// Find first nameMatch of packageNameRegexp.
	nameMatch := packageNameRegexp.FindStringSubmatch(string(content))
	if len(nameMatch) < 2 {
		return "", errors.New("failed extracting package name from content")
	}

	// Create package-version regexp.
	packageVersionRegexp, err := regexp.Compile(`(?m)^Version:\s(\w[\w-.]+)`)
	if err != nil {
		return "", err
	}

	// Find first match of packageNameRegexp.
	versionMatch := packageVersionRegexp.FindStringSubmatch(string(content))
	if len(versionMatch) < 2 {
		return "", errors.New("failed extracting package version from content")
	}

	return nameMatch[1] + ":" + versionMatch[1], nil
}


<|start_filename|>utils/command.go<|end_filename|>
package utils

import (
	"bytes"
	"fmt"
	"io"
	"os/exec"
	"strings"
)

type Command struct {
	Executable string
	CmdName    string
	CmdArgs    []string
	Dir        string
	StrWriter  io.WriteCloser
	ErrWriter  io.WriteCloser
}

func NewCommand(executable, cmdName string, cmdArgs []string) *Command {
	return &Command{Executable: executable, CmdName: cmdName, CmdArgs: cmdArgs}
}

func (config *Command) RunWithOutput() (data []byte, err error) {
	cmd := config.GetCmd()
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err = cmd.Run()
	if err != nil {
		return nil, fmt.Errorf("failed running command: '%s %s' with error: %s - %s",
			cmd.Path,
			strings.Join(cmd.Args, " "),
			err.Error(),
			stderr.String(),
		)
	}
	return stdout.Bytes(), nil
}

func (config *Command) GetCmd() (cmd *exec.Cmd) {
	var cmdStr []string
	if config.CmdName != "" {
		cmdStr = append(cmdStr, config.CmdName)
	}
	if config.CmdArgs != nil && len(config.CmdArgs) > 0 {
		cmdStr = append(cmdStr, config.CmdArgs...)
	}
	cmd = exec.Command(config.Executable, cmdStr...)
	cmd.Dir = config.Dir
	return
}

func (config *Command) GetEnv() map[string]string {
	return map[string]string{}
}

func (config *Command) GetStdWriter() io.WriteCloser {
	return config.StrWriter
}

func (config *Command) GetErrWriter() io.WriteCloser {
	return config.ErrWriter
}


<|start_filename|>utils/pythonutils/utils.go<|end_filename|>
package pythonutils

import (
	"errors"
	buildinfo "github.com/jfrog/build-info-go/entities"
	"strings"
)

const (
	Pip    = "pip"
	Pipenv = "pipenv"
)

type PythonTool string

// Parse pythonDependencyPackage list to dependencies map. (mapping dependency to his child deps)
// Also returns a list of project's root dependencies
func parseDependenciesToGraph(packages []pythonDependencyPackage) (map[string][]string, []string, error) {
	// Create packages map.
	packagesMap := map[string][]string{}
	allSubPackages := map[string]bool{}
	for _, pkg := range packages {
		var subPackages []string
		for _, subPkg := range pkg.Dependencies {
			subPkgFullName := subPkg.Key + ":" + subPkg.InstalledVersion
			subPackages = append(subPackages, subPkgFullName)
			allSubPackages[subPkgFullName] = true
		}
		packagesMap[pkg.Package.Key+":"+pkg.Package.InstalledVersion] = subPackages
	}

	var topLevelPackagesList []string
	for pkgName := range packagesMap {
		if !allSubPackages[pkgName] {
			topLevelPackagesList = append(topLevelPackagesList, pkgName)
		}
	}
	return packagesMap, topLevelPackagesList, nil
}

// Structs for parsing the pip-dependency-map result.
type pythonDependencyPackage struct {
	Package      packageType  `json:"package,omitempty"`
	Dependencies []dependency `json:"dependencies,omitempty"`
}

type packageType struct {
	Key              string `json:"key,omitempty"`
	PackageName      string `json:"package_name,omitempty"`
	InstalledVersion string `json:"installed_version,omitempty"`
}

type dependency struct {
	Key              string `json:"key,omitempty"`
	PackageName      string `json:"package_name,omitempty"`
	InstalledVersion string `json:"installed_version,omitempty"`
}

func GetPythonDependencies(tool PythonTool, srcPath, localDependenciesPath string) (dependenciesGraph map[string][]string, topLevelDependencies []string, err error) {
	switch tool {
	case Pip:
		return getPipDependencies(srcPath, localDependenciesPath)
	case Pipenv:
		return getPipenvDependencies(srcPath)
	default:
		return nil, nil, errors.New(string(tool) + " commands are not supported.")
	}
}

// Before running this function, dependency IDs may be the file names of the resolved python packages.
// Update build info dependency IDs and the requestedBy field.
// allDependencies      - Dependency name to Dependency map
// dependenciesGraph    - Dependency graph as built by 'pipdeptree' or 'pipenv graph'
// topLevelPackagesList - The direct dependencies
// packageName          - The resolved package name of the Python project, may be empty if we couldn't resolve it
// moduleName           - The input module name from the user, or the packageName
func UpdateDepsIdsAndRequestedBy(dependenciesMap map[string]buildinfo.Dependency, dependenciesGraph map[string][]string, topLevelPackagesList []string, packageName, moduleName string) {
	if packageName == "" {
		// Projects without setup.py
		dependenciesGraph[moduleName] = topLevelPackagesList
	} else if packageName != moduleName {
		// Projects with setup.py
		dependenciesGraph[moduleName] = dependenciesGraph[packageName]
	}
	rootModule := buildinfo.Dependency{Id: moduleName, RequestedBy: [][]string{{}}}
	updateDepsIdsAndRequestedBy(rootModule, dependenciesMap, dependenciesGraph)
}

func updateDepsIdsAndRequestedBy(parentDependency buildinfo.Dependency, dependenciesMap map[string]buildinfo.Dependency, dependenciesGraph map[string][]string) {
	for _, childId := range dependenciesGraph[parentDependency.Id] {
		childName := childId[0:strings.Index(childId, ":")]
		if childDep, ok := dependenciesMap[childName]; ok {
			if childDep.NodeHasLoop() || len(childDep.RequestedBy) >= buildinfo.RequestedByMaxLength {
				continue
			}
			// Update RequestedBy field from parent's RequestedBy.
			childDep.UpdateRequestedBy(parentDependency.Id, parentDependency.RequestedBy)

			// Set dependency type
			if childDep.Type == "" {
				fileType := ""
				if i := strings.LastIndex(childDep.Id, ".tar."); i != -1 {
					fileType = childDep.Id[i+1:]
				} else if i := strings.LastIndex(childDep.Id, "."); i != -1 {
					fileType = childDep.Id[i+1:]
				}
				childDep.Type = fileType
			}
			// Convert Id field from filename to dependency id
			childDep.Id = childId
			// Reassign map entry with new entry copy
			dependenciesMap[childName] = childDep
			// Run recursive call on child dependencies
			updateDepsIdsAndRequestedBy(childDep, dependenciesMap, dependenciesGraph)
		}
	}
}


<|start_filename|>build/service.go<|end_filename|>
package build

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"

	buildinfo "github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
)

const BuildsTempPath = "jfrog/builds/"

type BuildInfoService struct {
	tempDirPath string
	logger      utils.Log
}

func NewBuildInfoService() *BuildInfoService {
	return &BuildInfoService{tempDirPath: filepath.Join(os.TempDir(), BuildsTempPath), logger: &utils.NullLog{}}
}

func (bis *BuildInfoService) SetTempDirPath(tempDirPath string) {
	bis.tempDirPath = tempDirPath
}

func (bis *BuildInfoService) SetLogger(logger utils.Log) {
	bis.logger = logger
}

// GetOrCreateBuild gets a build from cache, or creates a new one if it doesn't exist.
// It's important to invoke this function at the very beginning of the build, so that the start time property in the build-info will be accurate.
func (bis *BuildInfoService) GetOrCreateBuild(buildName, buildNumber string) (*Build, error) {
	return bis.GetOrCreateBuildWithProject(buildName, buildNumber, "")
}

// GetOrCreateBuildWithProject gets a build from cache, or creates a new one if it doesn't exist.
// It's important to invoke this function at the very beginning of the build, so that the start time property in the build-info will be accurate.
func (bis *BuildInfoService) GetOrCreateBuildWithProject(buildName, buildNumber, projectKey string) (*Build, error) {
	if len(buildName) > 0 && len(buildNumber) > 0 {
		err := saveBuildGeneralDetails(buildName, buildNumber, projectKey, bis.tempDirPath, bis.logger)
		if err != nil {
			return nil, err
		}
	}
	return NewBuild(buildName, buildNumber, projectKey, bis.tempDirPath, bis.logger), nil
}

func saveBuildGeneralDetails(buildName, buildNumber, projectKey, buildsDirPath string, log utils.Log) error {
	partialsBuildDir, err := utils.GetPartialsBuildDir(buildName, buildNumber, projectKey, buildsDirPath)
	if err != nil {
		return err
	}
	log.Debug("Saving build general details at: " + partialsBuildDir)
	detailsFilePath := filepath.Join(partialsBuildDir, BuildInfoDetails)
	var exists bool
	exists, err = utils.IsFileExists(detailsFilePath, true)
	if err != nil || exists {
		return err
	}
	meta := buildinfo.General{
		Timestamp: time.Now(),
	}
	b, err := json.Marshal(&meta)
	if err != nil {
		return err
	}
	var content bytes.Buffer
	err = json.Indent(&content, b, "", "  ")
	if err != nil {
		return err
	}
	return ioutil.WriteFile(detailsFilePath, content.Bytes(), 0600)
}


<|start_filename|>utils/pythonutils/piputils_test.go<|end_filename|>
package pythonutils

import (
	testdatautils "github.com/jfrog/build-info-go/build/testdata"
	"github.com/stretchr/testify/assert"
	"path/filepath"
	"strings"
	"testing"
)

func TestGetProjectNameFromFileContent(t *testing.T) {
	tests := []struct {
		fileContent         string
		expectedProjectName string
	}{
		{"Metadata-Version: 1.0\nName: jfrog-python-example-1\nVersion: 1.0\nSummary: Project example for building Python project with JFrog products\nHome-page: https://github.com/jfrog/project-examples\nAuthor: JFrog\nAuthor-email: <EMAIL>\nLicense: UNKNOWN\nDescription: UNKNOWN\nPlatform: UNKNOWN", "jfrog-python-example-1:1.0"},
		{"Metadata-Version: Name: jfrog-python-example-2\nLicense: UNKNOWN\nDescription: UNKNOWN\nPlatform: UNKNOWN\nName: jfrog-python-example-2\nVersion: 1.0\nSummary: Project example for building Python project with JFrog products\nHome-page: https://github.com/jfrog/project-examples\nAuthor: JFrog\nAuthor-email: <EMAIL>", "jfrog-python-example-2:1.0"},
		{"Name:Metadata-Version: 3.0\nName: jfrog-python-example-3\nVersion: 1.0\nSummary: Project example for building Python project with JFrog products\nHome-page: https://github.com/jfrog/project-examples\nAuthor: JFrog\nAuthor-email: <EMAIL>\nName: jfrog-python-example-4", "jfrog-python-example-3:1.0"},
	}

	for _, test := range tests {
		actualValue, err := getProjectIdFromFileContent([]byte(test.fileContent))
		if err != nil {
			t.Error(err)
		}
		if actualValue != test.expectedProjectName {
			t.Errorf("Expected value: %s, got: %s.", test.expectedProjectName, actualValue)
		}
	}
}

var moduleNameTestProvider = []struct {
	projectName         string
	moduleName          string
	expectedModuleName  string
	expectedPackageName string
}{
	{"setuppyproject", "", "jfrog-python-example:1.0", "jfrog-python-example:1.0"},
	{"setuppyproject", "overidden-module", "overidden-module", "jfrog-python-example:1.0"},
	{"requirementsproject", "", "", ""},
	{"requirementsproject", "overidden-module", "overidden-module", ""},
}

func TestDetermineModuleName(t *testing.T) {
	for _, test := range moduleNameTestProvider {
		t.Run(strings.Join([]string{test.projectName, test.moduleName}, "/"), func(t *testing.T) {
			tmpProjectPath, cleanup := testdatautils.CreateTestProject(t, filepath.Join("..", "testdata", "pip", test.projectName))
			defer cleanup()

			// Determine module name
			packageName, err := GetPackageNameFromSetuppy(tmpProjectPath)
			assert.NoError(t, err)
			assert.Equal(t, test.expectedPackageName, packageName)
		})
	}
}


<|start_filename|>build/golang.go<|end_filename|>
package build

import (
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"unicode"

	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
)

type GoModule struct {
	containingBuild *Build
	name            string
	srcPath         string
}

func newGoModule(srcPath string, containingBuild *Build) (*GoModule, error) {
	var err error
	if srcPath == "" {
		srcPath, err = utils.GetProjectRoot()
		if err != nil {
			return nil, err
		}
	}

	// Read module name
	name, err := utils.GetModuleNameByDir(srcPath, containingBuild.logger)
	if err != nil {
		return nil, err
	}

	return &GoModule{name: name, srcPath: srcPath, containingBuild: containingBuild}, nil
}

func (gm *GoModule) CalcDependencies() error {
	if !gm.containingBuild.buildNameAndNumberProvided() {
		return errors.New("a build name must be provided in order to collect the project's dependencies")
	}
	buildInfoDependencies, err := gm.loadDependencies()
	if err != nil {
		return err
	}

	buildInfoModule := entities.Module{Id: gm.name, Type: entities.Go, Dependencies: buildInfoDependencies}
	buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}

	return gm.containingBuild.SaveBuildInfo(buildInfo)
}

func (gm *GoModule) SetName(name string) {
	gm.name = name
}

func (gm *GoModule) AddArtifacts(artifacts ...entities.Artifact) error {
	if !gm.containingBuild.buildNameAndNumberProvided() {
		return errors.New("a build name must be provided in order to add artifacts")
	}
	partial := &entities.Partial{ModuleId: gm.name, ModuleType: entities.Go, Artifacts: artifacts}
	return gm.containingBuild.SavePartialBuildInfo(partial)
}

func (gm *GoModule) loadDependencies() ([]entities.Dependency, error) {
	cachePath, err := utils.GetCachePath()
	if err != nil {
		return nil, err
	}
	dependenciesGraph, err := utils.GetDependenciesGraph(gm.srcPath, gm.containingBuild.logger)
	if err != nil {
		return nil, err
	}
	dependenciesMap, err := gm.getGoDependencies(cachePath)
	if err != nil {
		return nil, err
	}
	emptyRequestedBy := [][]string{{}}
	populateRequestedByField(gm.name, emptyRequestedBy, dependenciesMap, dependenciesGraph)
	return dependenciesMapToList(dependenciesMap), nil
}

func (gm *GoModule) getGoDependencies(cachePath string) (map[string]entities.Dependency, error) {
	modulesMap, err := utils.GetDependenciesList(gm.srcPath, gm.containingBuild.logger)
	if err != nil || len(modulesMap) == 0 {
		return nil, err
	}
	// Create a map from dependency to parents
	buildInfoDependencies := make(map[string]entities.Dependency)
	for moduleId := range modulesMap {
		// If the path includes capital letters, the Go convention is to use "!" before the letter. The letter itself is in lowercase.
		encodedDependencyId := goModEncode(moduleId)

		// We first check if this dependency has a zip in the local Go cache.
		// If it does not, nil is returned. This seems to be a bug in Go.
		zipPath, err := gm.getPackageZipLocation(cachePath, encodedDependencyId)
		if err != nil {
			return nil, err
		}
		if zipPath == "" {
			continue
		}
		zipDependency, err := populateZip(encodedDependencyId, zipPath)
		if err != nil {
			return nil, err
		}
		buildInfoDependencies[moduleId] = zipDependency
	}
	return buildInfoDependencies, nil
}

// Returns the actual path to the dependency.
// If the path includes capital letters, the Go convention is to use "!" before the letter.
// The letter itself is in lowercase.
func goModEncode(name string) string {
	path := ""
	for _, letter := range name {
		if unicode.IsUpper(letter) {
			path += "!" + strings.ToLower(string(letter))
		} else {
			path += string(letter)
		}
	}
	return path
}

// Returns the path to the package zip file if exists.
func (gm *GoModule) getPackageZipLocation(cachePath, encodedDependencyId string) (string, error) {
	zipPath, err := gm.getPackagePathIfExists(cachePath, encodedDependencyId)
	if err != nil {
		return "", err
	}

	if zipPath != "" {
		return zipPath, nil
	}

	return gm.getPackagePathIfExists(filepath.Dir(cachePath), encodedDependencyId)
}

// Validates that the package zip file exists and returns its path.
func (gm *GoModule) getPackagePathIfExists(cachePath, encodedDependencyId string) (zipPath string, err error) {
	moduleInfo := strings.Split(encodedDependencyId, ":")
	if len(moduleInfo) != 2 {
		gm.containingBuild.logger.Debug("The encoded dependency Id syntax should be 'name:version' but instead got:", encodedDependencyId)
		return "", nil
	}
	dependencyName := moduleInfo[0]
	version := moduleInfo[1]
	zipPath = filepath.Join(cachePath, dependencyName, "@v", version+".zip")
	fileExists, err := utils.IsFileExists(zipPath, true)
	if err != nil {
		return "", fmt.Errorf("could not find zip binary for dependency '%s' at %s: %s", dependencyName, zipPath, err)
	}
	// Zip binary does not exist, so we skip it by returning a nil dependency.
	if !fileExists {
		gm.containingBuild.logger.Debug("The following file is missing:", zipPath)
		return "", nil
	}
	return zipPath, nil
}

// populateZip adds the zip file as build-info dependency
func populateZip(packageId, zipPath string) (zipDependency entities.Dependency, err error) {
	// Zip file dependency for the build-info
	zipDependency = entities.Dependency{Id: packageId}
	md5, sha1, sha2, err := utils.GetFileChecksums(zipPath)
	if err != nil {
		return
	}
	zipDependency.Type = "zip"
	zipDependency.Checksum = entities.Checksum{Sha1: sha1, Md5: md5, Sha256: sha2}
	return
}

func populateRequestedByField(parentId string, parentRequestedBy [][]string, dependenciesMap map[string]entities.Dependency, dependenciesGraph map[string][]string) {
	for _, childName := range dependenciesGraph[parentId] {
		if childDep, ok := dependenciesMap[childName]; ok {
			if childDep.NodeHasLoop() || len(childDep.RequestedBy) >= entities.RequestedByMaxLength {
				continue
			}
			// Update RequestedBy field from parent's RequestedBy.
			childDep.UpdateRequestedBy(parentId, parentRequestedBy)
			// Reassign map entry with new entry copy
			dependenciesMap[childName] = childDep
			// Run recursive call on child dependencies
			populateRequestedByField(childName, childDep.RequestedBy, dependenciesMap, dependenciesGraph)
		}
	}
}


<|start_filename|>main.go<|end_filename|>
package main

import (
	"os"

	"github.com/jfrog/build-info-go/cli"
	"github.com/jfrog/build-info-go/utils"
	clitool "github.com/urfave/cli/v2"
)

const logLevelEnv = "BUILD_INFO_LOG_LEVEL"

var log utils.Log
var cliVersion = "1.2.6"

func main() {
	log = utils.NewDefaultLogger(getCliLogLevel())
	app := &clitool.App{
		Name:     "Build-Info CLI",
		Usage:    "Generate build-info for your source code",
		Commands: cli.GetCommands(log),
		Version:  cliVersion,
	}
	err := app.Run(os.Args)
	if err != nil {
		log.Error(err)
		os.Exit(1)
	}
}

func getCliLogLevel() utils.LevelType {
	switch os.Getenv(logLevelEnv) {
	case "ERROR":
		return utils.ERROR
	case "WARN":
		return utils.WARN
	case "DEBUG":
		return utils.DEBUG
	default:
		return utils.INFO
	}
}


<|start_filename|>entities/buildinfo.go<|end_filename|>
package entities

import (
	"regexp"
	"strings"
	"time"

	"github.com/pkg/errors"

	cdx "github.com/CycloneDX/cyclonedx-go"
	"github.com/jfrog/build-info-go/utils"
	"github.com/jfrog/gofrog/stringutils"
)

type ModuleType string

const (
	TimeFormat           = "2006-01-02T15:04:05.000-0700"
	BuildInfoEnvPrefix   = "buildInfo.env."
	RequestedByMaxLength = 10

	// Build type
	Build ModuleType = "build"

	// Package managers types
	Generic ModuleType = "generic"
	Maven   ModuleType = "maven"
	Gradle  ModuleType = "gradle"
	Docker  ModuleType = "docker"
	Npm     ModuleType = "npm"
	Nuget   ModuleType = "nuget"
	Go      ModuleType = "go"
	Python  ModuleType = "python"
)

type BuildInfo struct {
	Name          string   `json:"name,omitempty"`
	Number        string   `json:"number,omitempty"`
	Agent         *Agent   `json:"agent,omitempty"`
	BuildAgent    *Agent   `json:"buildAgent,omitempty"`
	Modules       []Module `json:"modules,omitempty"`
	Started       string   `json:"started,omitempty"`
	Properties    Env      `json:"properties,omitempty"`
	Principal     string   `json:"artifactoryPrincipal,omitempty"`
	BuildUrl      string   `json:"url,omitempty"`
	Issues        *Issues  `json:"issues,omitempty"`
	PluginVersion string   `json:"artifactoryPluginVersion,omitempty"`
	VcsList       []Vcs    `json:"vcs,omitempty"`
}

func New() *BuildInfo {
	return &BuildInfo{
		Agent:      &Agent{},
		BuildAgent: &Agent{Name: "GENERIC"},
		Modules:    make([]Module, 0),
		VcsList:    make([]Vcs, 0),
	}
}

func (targetBuildInfo *BuildInfo) SetBuildAgentVersion(buildAgentVersion string) {
	targetBuildInfo.BuildAgent.Version = buildAgentVersion
}

func (targetBuildInfo *BuildInfo) SetAgentName(agentName string) {
	targetBuildInfo.Agent.Name = agentName
}

func (targetBuildInfo *BuildInfo) SetAgentVersion(agentVersion string) {
	targetBuildInfo.Agent.Version = agentVersion
}

func (targetBuildInfo *BuildInfo) SetPluginVersion(pluginVersion string) {
	targetBuildInfo.PluginVersion = pluginVersion
}

// Append the modules of the received build info to this build info.
// If the two build info instances contain modules with identical names, these modules are merged.
// When merging the modules, the artifacts and dependencies remain unique according to their checksum.
func (targetBuildInfo *BuildInfo) Append(buildInfo *BuildInfo) {
	for i, newModule := range buildInfo.Modules {
		exists := false
		for j := range targetBuildInfo.Modules {
			if newModule.Id == targetBuildInfo.Modules[j].Id {
				mergeModules(&buildInfo.Modules[i], &targetBuildInfo.Modules[j])
				exists = true
				break
			}
		}
		if !exists {
			targetBuildInfo.Modules = append(targetBuildInfo.Modules, newModule)
		}
	}
}

// IncludeEnv gets one or more wildcard patterns and filters out environment variables that don't match any of them.
func (targetBuildInfo *BuildInfo) IncludeEnv(patterns ...string) error {
	var err error
	for key := range targetBuildInfo.Properties {
		if !strings.HasPrefix(key, BuildInfoEnvPrefix) {
			continue
		}
		envKey := strings.TrimPrefix(key, BuildInfoEnvPrefix)
		include := false
		for _, filterPattern := range patterns {
			include, err = stringutils.MatchWildcardPattern(strings.ToLower(filterPattern), strings.ToLower(envKey))
			if err != nil {
				return err
			}
			if include {
				break
			}
		}

		if !include {
			delete(targetBuildInfo.Properties, key)
		}
	}
	return nil
}

// ExcludeEnv gets one or more wildcard patterns and filters out environment variables that match at least one of them.
func (targetBuildInfo *BuildInfo) ExcludeEnv(patterns ...string) error {
	for key := range targetBuildInfo.Properties {
		if !strings.HasPrefix(key, BuildInfoEnvPrefix) {
			continue
		}
		envKey := strings.TrimPrefix(key, BuildInfoEnvPrefix)
		for _, filterPattern := range patterns {
			match, err := stringutils.MatchWildcardPattern(strings.ToLower(filterPattern), strings.ToLower(envKey))
			if err != nil {
				return err
			}
			if match {
				delete(targetBuildInfo.Properties, key)
				break
			}
		}
	}
	return nil
}

func (targetBuildInfo *BuildInfo) ToCycloneDxBom() (*cdx.BOM, error) {
	var biDependencies []Dependency
	moduleIds := make(map[string]bool)

	for _, module := range targetBuildInfo.Modules {
		// Aggregated builds are not supported
		if module.Type == Build {
			continue
		}

		moduleDep := Dependency{Id: module.Id}
		moduleIds[module.Id] = true

		dependenciesToAdd := []Dependency{moduleDep}
		dependenciesToAdd = append(dependenciesToAdd, module.Dependencies...)
		mergeDependenciesLists(&dependenciesToAdd, &biDependencies)
	}

	var components []cdx.Component
	depMap := make(map[string]map[string]bool)
	for _, biDep := range biDependencies {
		newComp, err := packageIdToCycloneDxComponent(biDep.Id)
		if err != nil {
			return nil, err
		}
		newComp.BOMRef = biDep.Id
		if _, exist := moduleIds[biDep.Id]; exist {
			newComp.Type = cdx.ComponentTypeApplication
		} else {
			newComp.Type = cdx.ComponentTypeLibrary
		}

		if !biDep.Checksum.IsEmpty() {
			hashes := []cdx.Hash{
				{
					Algorithm: cdx.HashAlgoSHA256,
					Value:     biDep.Sha256,
				},
				{
					Algorithm: cdx.HashAlgoSHA1,
					Value:     biDep.Sha1,
				},
				{
					Algorithm: cdx.HashAlgoMD5,
					Value:     biDep.Md5,
				},
			}
			newComp.Hashes = &hashes
		}
		components = append(components, *newComp)

		for _, reqByPath := range biDep.RequestedBy {
			if depMap[reqByPath[0]] == nil {
				depMap[reqByPath[0]] = make(map[string]bool)
			}
			depMap[reqByPath[0]][biDep.Id] = true
		}
	}

	// Convert the map of dependencies to CycloneDX dependency objects
	var dependencies []cdx.Dependency
	for compRef, deps := range depMap {
		var cdxDepsSlice []cdx.Dependency
		for depRef := range deps {
			cdxDepsSlice = append(cdxDepsSlice, cdx.Dependency{Ref: depRef})
		}
		dependencies = append(dependencies, cdx.Dependency{Ref: compRef, Dependencies: &cdxDepsSlice})
	}

	bom := cdx.NewBOM()
	bom.Components = &components
	bom.Dependencies = &dependencies
	return bom, nil
}

func packageIdToCycloneDxComponent(packageId string) (*cdx.Component, error) {
	comp := &cdx.Component{}
	packageIdParts := strings.Split(packageId, ":")
	switch len(packageIdParts) {
	case 1:
		comp.Name = packageIdParts[0]
	case 2:
		comp.Name = packageIdParts[0]
		comp.Version = packageIdParts[1]
	case 3:
		comp.Group = packageIdParts[0]
		comp.Name = packageIdParts[1]
		comp.Version = packageIdParts[2]
	default:
		return nil, errors.New("invalid package identifier: " + packageId)
	}
	return comp, nil
}

// Merge the first module into the second module.
func mergeModules(merge *Module, into *Module) {
	mergeArtifacts(&merge.Artifacts, &into.Artifacts)
	mergeArtifacts(&merge.ExcludedArtifacts, &into.ExcludedArtifacts)
	mergeDependenciesLists(&merge.Dependencies, &into.Dependencies)
}

func mergeArtifacts(mergeArtifacts *[]Artifact, intoArtifacts *[]Artifact) {
	for _, mergeArtifact := range *mergeArtifacts {
		exists := false
		for _, artifact := range *intoArtifacts {
			if mergeArtifact.Sha1 == artifact.Sha1 {
				exists = true
				break
			}
		}
		if !exists {
			*intoArtifacts = append(*intoArtifacts, mergeArtifact)
		}
	}
}

func mergeDependenciesLists(dependenciesToAdd, intoDependencies *[]Dependency) {
	for i, dependencyToAdd := range *dependenciesToAdd {
		exists := false
		for _, dependency := range *intoDependencies {
			if dependencyToAdd.Sha1 == dependency.Sha1 && dependencyToAdd.Id == dependency.Id {
				exists = true
				(*dependenciesToAdd)[i] = mergeDependencies(dependency, dependencyToAdd)
				break
			}
		}
		if !exists {
			*intoDependencies = append(*intoDependencies, dependencyToAdd)
		}
	}
}

func mergeDependencies(dep1, dep2 Dependency) Dependency {
	return Dependency{
		Id:          dep1.Id,
		Type:        dep1.Type,
		Scopes:      mergeStringSlices(dep1.Scopes, dep2.Scopes),
		RequestedBy: mergeRequestedBySlices(dep1.RequestedBy, dep2.RequestedBy),
		Checksum:    dep1.Checksum,
	}
}

func mergeStringSlices(slice1, slice2 []string) []string {
	for _, item2 := range slice2 {
		exists := false
		for _, item1 := range slice1 {
			if item1 == item2 {
				exists = true
				break
			}
			if !exists {
				slice1 = append(slice1, item2)
			}
		}
	}
	return slice1
}

// mergeRequestedBySlices gets two slices of dependencies' paths in a build (RequestedBy) and merges them together, without duplicates.
func mergeRequestedBySlices(requestedBy1, requestedBy2 [][]string) [][]string {
	for _, item2 := range requestedBy2 {
		exists := false
		for _, item1 := range requestedBy1 {
			if equalStringSlices(item1, item2) {
				exists = true
				break
			}
			if !exists {
				requestedBy1 = append(requestedBy1, item2)
			}
		}
	}
	return requestedBy1
}

func equalStringSlices(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// PublishedBuildInfo represents the response structure returned from Artifactory when getting a build-info.
type PublishedBuildInfo struct {
	Uri       string    `json:"uri,omitempty"`
	BuildInfo BuildInfo `json:"buildInfo,omitempty"`
}

type Agent struct {
	Name    string `json:"name,omitempty"`
	Version string `json:"version,omitempty"`
}

type Module struct {
	Type              ModuleType   `json:"type,omitempty"`
	Properties        interface{}  `json:"properties,omitempty"`
	Id                string       `json:"id,omitempty"`
	Artifacts         []Artifact   `json:"artifacts,omitempty"`
	ExcludedArtifacts []Artifact   `json:"excludedArtifacts,omitempty"`
	Dependencies      []Dependency `json:"dependencies,omitempty"`
	// Used in aggregated builds - this field stores the checksums of the referenced build-info JSON.
	Checksum
}

// If the 'other' Module matches the current one, return true.
// 'other' Module may contain regex values for Id, Artifacts, ExcludedArtifacts, Dependencies and Checksum.
func (m *Module) isEqual(other Module) (bool, error) {
	match, err := m.Checksum.IsEqual(other.Checksum)
	if !match || err != nil {
		return false, err
	}
	match, err = isEqualArtifactSlices(m.Artifacts, other.Artifacts)
	if !match || err != nil {
		return false, err
	}
	match, err = isEqualArtifactSlices(m.ExcludedArtifacts, other.ExcludedArtifacts)
	if !match || err != nil {
		return false, err
	}
	match, err = isEqualDependencySlices(m.Dependencies, other.Dependencies)
	if !match || err != nil {
		return false, err
	}
	match, err = regexp.MatchString(m.Id, other.Id)
	if !match || err != nil {
		return false, err
	}
	return m.Type == other.Type, nil
}

func IsEqualModuleSlices(actual, other []Module) (bool, error) {
	return isEqualModuleSlices(actual, other)
}

func isEqualModuleSlices(actual, other []Module) (bool, error) {
	if len(actual) != len(other) {
		return false, nil
	}
	visitedIndexes := make(map[int]bool)
	for _, aEl := range actual {
		found := false
		for index, eEl := range other {
			if _, ok := visitedIndexes[index]; !ok {
				match, err := aEl.isEqual(eEl)
				if err != nil {
					return false, err
				}
				if match {
					found = true
					visitedIndexes[index] = true
					break
				}
			}
		}
		if !found {
			return false, nil
		}
	}
	return true, nil
}

type Artifact struct {
	Name string `json:"name,omitempty"`
	Type string `json:"type,omitempty"`
	Path string `json:"path,omitempty"`
	Checksum
}

// If the 'other' Artifact matches the current one, return true.
// 'other' Artifacts may contain regex values for Name, Path, and Checksum.
func (a *Artifact) isEqual(other Artifact) (bool, error) {
	match, err := regexp.MatchString(other.Name, a.Name)
	if !match || err != nil {
		return false, err
	}
	match, err = regexp.MatchString(other.Path, a.Path)
	if !match || err != nil {
		return false, err
	}
	match, err = a.Checksum.IsEqual(other.Checksum)
	if !match || err != nil {
		return false, err
	}

	return a.Type == other.Type, nil
}

func isEqualArtifactSlices(actual, other []Artifact) (bool, error) {
	if len(actual) != len(other) {
		return false, nil
	}
	visitedIndexes := make(map[int]bool)
	for _, aEl := range actual {
		found := false
		for index, eEl := range other {
			if _, ok := visitedIndexes[index]; !ok {
				match, err := aEl.isEqual(eEl)
				if err != nil {
					return false, err
				}
				if match {
					found = true
					visitedIndexes[index] = true
					break
				}
			}
		}
		if !found {
			return false, nil
		}
	}
	return true, nil
}

type Dependency struct {
	Id          string     `json:"id,omitempty"`
	Type        string     `json:"type,omitempty"`
	Scopes      []string   `json:"scopes,omitempty"`
	RequestedBy [][]string `json:"requestedBy,omitempty"`
	Checksum
}

// If the 'other' Dependency matches the current one, return true.
// 'other' Dependency may contain regex values for Id and Checksum.
func (d *Dependency) IsEqual(other Dependency) (bool, error) {
	match, err := d.Checksum.IsEqual(other.Checksum)
	if !match || err != nil {
		return false, err
	}
	match, err = regexp.MatchString(other.Id, d.Id)
	if !match || err != nil {
		return false, err
	}
	return d.Type == other.Type && utils.IsEqualSlices(d.Scopes, other.Scopes) && utils.IsEqual2DSlices(d.RequestedBy, other.RequestedBy), nil
}

func IsEqualDependencySlices(actual, other []Dependency) (bool, error) {
	return IsEqualModuleSlices([]Module{{Dependencies: actual}}, []Module{{Dependencies: other}})
}

func isEqualDependencySlices(actual, other []Dependency) (bool, error) {
	if len(actual) != len(other) {
		return false, nil
	}
	visitedIndexes := make(map[int]bool)
	for _, aEl := range actual {
		found := false
		for index, eEl := range other {
			if _, ok := visitedIndexes[index]; !ok {
				match, err := aEl.IsEqual(eEl)
				if err != nil {
					return false, err
				}
				if match {
					found = true
					visitedIndexes[index] = true
					break
				}
			}
		}
		if !found {
			return false, nil
		}
	}
	return true, nil
}
func (d *Dependency) UpdateRequestedBy(parentId string, parentRequestedBy [][]string) {
	// Filter all existing paths from parent
	var filteredChildRequestedBy [][]string
	for _, childRequestedBy := range d.RequestedBy {
		if childRequestedBy[0] != parentId {
			filteredChildRequestedBy = append(filteredChildRequestedBy, childRequestedBy)
		}
	}
	// Add all updated paths from parent
	for _, requestedBy := range parentRequestedBy {
		newRequestedBy := append([]string{parentId}, requestedBy...)
		filteredChildRequestedBy = append(filteredChildRequestedBy, newRequestedBy)
	}
	d.RequestedBy = filteredChildRequestedBy
}

func (d *Dependency) NodeHasLoop() bool {
	for _, requestedBy := range d.RequestedBy {
		for _, depId := range requestedBy {
			if depId == d.Id {
				return true
			}
		}
	}
	return false
}

type Issues struct {
	Tracker                *Tracker        `json:"tracker,omitempty"`
	AggregateBuildIssues   bool            `json:"aggregateBuildIssues,omitempty"`
	AggregationBuildStatus string          `json:"aggregationBuildStatus,omitempty"`
	AffectedIssues         []AffectedIssue `json:"affectedIssues,omitempty"`
}

type Tracker struct {
	Name    string `json:"name,omitempty"`
	Version string `json:"version,omitempty"`
}

type AffectedIssue struct {
	Key        string `json:"key,omitempty"`
	Url        string `json:"url,omitempty"`
	Summary    string `json:"summary,omitempty"`
	Aggregated bool   `json:"aggregated,omitempty"`
}

type Checksum struct {
	Sha1   string `json:"sha1,omitempty"`
	Md5    string `json:"md5,omitempty"`
	Sha256 string `json:"sha256,omitempty"`
}

func (c *Checksum) IsEmpty() bool {
	return c.Md5 == "" && c.Sha1 == "" && c.Sha256 == ""
}

// If the 'other' checksum matches the current one, return true.
// 'other' checksum may contain regex values for sha1, sha256 and md5.
func (c *Checksum) IsEqual(other Checksum) (bool, error) {
	match, err := regexp.MatchString(other.Md5, c.Md5)
	if !match || err != nil {
		return false, err
	}
	match, err = regexp.MatchString(other.Sha1, c.Sha1)
	if !match || err != nil {
		return false, err
	}
	match, err = regexp.MatchString(other.Sha256, c.Sha256)
	if !match || err != nil {
		return false, err
	}

	return true, nil
}

type Env map[string]string

type Vcs struct {
	Url      string `json:"url,omitempty"`
	Revision string `json:"revision,omitempty"`
	Branch   string `json:"branch,omitempty"`
	Message  string `json:"message,omitempty"`
}

type Partials []*Partial

type Partial struct {
	ModuleType   ModuleType   `json:"Type,omitempty"`
	Artifacts    []Artifact   `json:"Artifacts,omitempty"`
	Dependencies []Dependency `json:"Dependencies,omitempty"`
	Env          Env          `json:"Env,omitempty"`
	Timestamp    int64        `json:"Timestamp,omitempty"`
	ModuleId     string       `json:"ModuleId,omitempty"`
	Issues       *Issues      `json:"Issues,omitempty"`
	VcsList      []Vcs        `json:"vcs,omitempty"`
	Checksum
}

func (partials Partials) Len() int {
	return len(partials)
}

func (partials Partials) Less(i, j int) bool {
	return partials[i].Timestamp < partials[j].Timestamp
}

func (partials Partials) Swap(i, j int) {
	partials[i], partials[j] = partials[j], partials[i]
}

type General struct {
	Timestamp time.Time `json:"Timestamp,omitempty"`
}


<|start_filename|>cli/cli.go<|end_filename|>
package cli

import (
	"bytes"
	"encoding/json"
	"fmt"
	cdx "github.com/CycloneDX/cyclonedx-go"
	"github.com/jfrog/build-info-go/build"
	"github.com/jfrog/build-info-go/utils"
	"github.com/jfrog/build-info-go/utils/pythonutils"
	"github.com/pkg/errors"
	clitool "github.com/urfave/cli/v2"
	"os"
	"os/exec"
	"strings"
)

const (
	formatFlag    = "format"
	cycloneDxXml  = "cyclonedx/xml"
	cycloneDxJson = "cyclonedx/json"
)

func GetCommands(logger utils.Log) []*clitool.Command {
	flags := []clitool.Flag{
		&clitool.StringFlag{
			Name:  formatFlag,
			Usage: fmt.Sprintf("[Optional] Set to convert the build-info to a different format. Supported values are '%s' and '%s'.` `", cycloneDxXml, cycloneDxJson),
		},
	}

	return []*clitool.Command{
		{
			Name:      "go",
			Usage:     "Generate build-info for a Go project",
			UsageText: "bi go",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("go-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				goModule, err := bld.AddGoModule("")
				if err != nil {
					return
				}
				err = goModule.CalcDependencies()
				if err != nil {
					return
				}
				return printBuild(bld, context.String(formatFlag))
			},
		},
		{
			Name:      "mvn",
			Usage:     "Generate build-info for a Maven project",
			UsageText: "bi mvn",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("mvn-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				mavenModule, err := bld.AddMavenModule("")
				if err != nil {
					return
				}
				err = mavenModule.CalcDependencies()
				if err != nil {
					return
				}
				return printBuild(bld, context.String(formatFlag))
			},
		},
		{
			Name:      "gradle",
			Usage:     "Generate build-info for a Gradle project",
			UsageText: "bi gradle",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("gradle-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				gradleModule, err := bld.AddGradleModule("")
				if err != nil {
					return
				}
				err = gradleModule.CalcDependencies()
				if err != nil {
					return
				}
				return printBuild(bld, context.String(formatFlag))
			},
		},
		{
			Name:      "npm",
			Usage:     "Generate build-info for an npm project",
			UsageText: "bi npm",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("npm-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				npmModule, err := bld.AddNpmModule("")
				if err != nil {
					return
				}
				err = npmModule.CalcDependencies()
				if err != nil {
					return
				}
				return printBuild(bld, context.String(formatFlag))
			},
		},
		{
			Name:            "yarn",
			Usage:           "Build a Yarn project and generate build-info for it",
			UsageText:       "bi yarn [yarn command] [command options]",
			Flags:           flags,
			SkipFlagParsing: true,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("yarn-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				yarnModule, err := bld.AddYarnModule("")
				if err != nil {
					return
				}
				formatValue, filteredArgs, err := extractStringFlag(context.Args().Slice(), formatFlag)
				if err != nil {
					return
				}
				yarnModule.SetArgs(filteredArgs)
				err = yarnModule.Build()
				if err != nil {
					return
				}
				return printBuild(bld, formatValue)
			},
		},
		{
			Name:      "pip",
			Usage:     "Generate build-info for a pip project",
			UsageText: "bi pip",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("pip-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				pythonModule, err := bld.AddPythonModule("", pythonutils.Pip)
				if err != nil {
					return
				}
				filteredArgs := filterCliFlags(context.Args().Slice(), flags)
				if filteredArgs[0] == "install" {
					err = pythonModule.RunInstallAndCollectDependencies(filteredArgs[1:])
					if err != nil {
						return
					}
					return printBuild(bld, context.String(formatFlag))
				} else {
					return exec.Command("pip", filteredArgs[1:]...).Run()
				}
			},
		},
		{
			Name:      "pipenv",
			Usage:     "Generate build-info for a pipenv project",
			UsageText: "bi pipenv",
			Flags:     flags,
			Action: func(context *clitool.Context) (err error) {
				service := build.NewBuildInfoService()
				service.SetLogger(logger)
				bld, err := service.GetOrCreateBuild("pipenv-build", "1")
				if err != nil {
					return
				}
				defer func() {
					e := bld.Clean()
					if err == nil {
						err = e
					}
				}()
				pythonModule, err := bld.AddPythonModule("", pythonutils.Pipenv)
				if err != nil {
					return
				}
				filteredArgs := filterCliFlags(context.Args().Slice(), flags)
				if filteredArgs[0] == "install" {
					err = pythonModule.RunInstallAndCollectDependencies(filteredArgs[1:])
					if err != nil {
						return
					}
					return printBuild(bld, context.String(formatFlag))
				} else {
					return exec.Command("pipenv", filteredArgs[1:]...).Run()
				}
			},
		},
	}
}

func printBuild(bld *build.Build, format string) error {
	buildInfo, err := bld.ToBuildInfo()
	if err != nil {
		return err
	}

	switch format {
	case cycloneDxXml:
		cdxBom, err := buildInfo.ToCycloneDxBom()
		if err != nil {
			return err
		}
		encoder := cdx.NewBOMEncoder(os.Stdout, cdx.BOMFileFormatXML)
		encoder.SetPretty(true)
		if err = encoder.Encode(cdxBom); err != nil {
			return err
		}
	case cycloneDxJson:
		cdxBom, err := buildInfo.ToCycloneDxBom()
		if err != nil {
			return err
		}
		encoder := cdx.NewBOMEncoder(os.Stdout, cdx.BOMFileFormatJSON)
		encoder.SetPretty(true)
		if err = encoder.Encode(cdxBom); err != nil {
			return err
		}
	case "":
		b, err := json.Marshal(buildInfo)
		if err != nil {
			return err
		}
		var content bytes.Buffer
		err = json.Indent(&content, b, "", "  ")
		if err != nil {
			return err
		}
		fmt.Println(content.String())
	default:
		return fmt.Errorf("'%s' is not a valid value for '%s'", format, formatFlag)
	}

	return nil
}

func extractStringFlag(args []string, flagName string) (flagValue string, filteredArgs []string, err error) {
	filteredArgs = []string{}
	for argIndex := 0; argIndex < len(args); argIndex++ {
		fullFlagName := "--" + flagName
		if args[argIndex] == fullFlagName {
			if len(args) <= argIndex+1 || strings.HasPrefix(args[argIndex+1], "-") {
				return "", nil, errors.New("Failed extracting value of provided flag: " + flagName)
			}
			flagValue = args[argIndex+1]
			argIndex++
		} else if argPrefix := fullFlagName + "="; strings.HasPrefix(args[argIndex], argPrefix) {
			flagValue = strings.TrimPrefix(args[argIndex], argPrefix)
		} else {
			filteredArgs = append(filteredArgs, args[argIndex])
		}
	}
	return
}

func filterCliFlags(allArgs []string, cliFlags []clitool.Flag) []string {
	var filteredArgs []string
	for _, arg := range allArgs {
		if !hasFlag(cliFlags, arg) {
			filteredArgs = append(filteredArgs, arg)
		}
	}
	return filteredArgs
}

func hasFlag(flagsList []clitool.Flag, arg string) bool {
	for _, flag := range flagsList {
		for _, name := range flag.Names() {
			if name == arg {
				return true
			}
		}
	}
	return false
}


<|start_filename|>utils/pythonutils/pipdeptree/scriptcreator.go<|end_filename|>
package main

import (
	"github.com/jfrog/build-info-go/utils"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"strings"
)

const (
	pipDepTreeContentFileName = "deptreescript.go"
	pipDepTreePythonScript    = "pipdeptree.py"
	pythonPackageName         = "pythonutils"
	pythonPackageRelativePath = "utils"
	// The pip-dep-tree script version. The version should be manually incremented following changes to the pipdeptree.py source file.
	pipDepTreeVersion = "5"
)

// This main function should be executed manually following changes in pipdeptree.py. Running the function generates new 'pipDepTreeContentFileName' from 'pipDepTreePythonScript.
// Make sure to increment the value of the 'pipDepTreeVersion' constant before running this main function.
func main() {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	// Check if a pip-dep-tree file of the latest version already exists
	pipDepTreeContentPath := filepath.Join(wd, pythonPackageRelativePath, pythonPackageName, pipDepTreeContentFileName)
	exists, err := utils.IsFileExists(pipDepTreeContentPath, false)
	if err != nil {
		panic(err)
	}
	if exists {
		return
	}
	// Read the script content from the .py file
	pyFile, err := ioutil.ReadFile(path.Join(wd, pythonPackageRelativePath, pythonPackageName, "pipdeptree", pipDepTreePythonScript))
	if err != nil {
		panic(err)
	}
	// Replace all backticks ( ` ) with a single quote ( ' )
	pyFileString := strings.ReplaceAll(string(pyFile), "`", "'")

	resourceString := "package " + pythonPackageName + "\n\n"
	// Add a const string with the script's version
	resourceString += "const pipDepTreeVersion = \"" + pipDepTreeVersion + "\"\n\n"
	// Write the script content a a byte-slice
	resourceString += "var pipDepTreeContent = []byte(`\n" + pyFileString + "`)"
	// Create .go file with the script content
	err = ioutil.WriteFile(pipDepTreeContentPath, []byte(resourceString), os.ModePerm)
	if err != nil {
		panic(err)
	}
}


<|start_filename|>build/npm.go<|end_filename|>
package build

import (
	"errors"
	"os"

	buildutils "github.com/jfrog/build-info-go/build/utils"
	"github.com/jfrog/build-info-go/entities"
	"github.com/jfrog/build-info-go/utils"
)

const minSupportedNpmVersion = "5.4.0"

type NpmModule struct {
	containingBuild *Build
	name            string
	srcPath         string
	executablePath  string
	npmArgs         []string
}

// Pass an empty string for srcPath to find the npm project in the working directory.
func newNpmModule(srcPath string, containingBuild *Build) (*NpmModule, error) {
	npmVersion, executablePath, err := buildutils.GetNpmVersionAndExecPath(containingBuild.logger)
	if err != nil {
		return nil, err
	}
	if npmVersion.Compare(minSupportedNpmVersion) > 0 {
		return nil, errors.New("npm CLI must have version " + minSupportedNpmVersion + " or higher. The current version is: " + npmVersion.GetVersion())
	}

	if srcPath == "" {
		wd, err := os.Getwd()
		if err != nil {
			return nil, err
		}
		srcPath, err = utils.FindFileInDirAndParents(wd, "package.json")
		if err != nil {
			return nil, err
		}
	}

	// Read module name
	packageInfo, err := buildutils.ReadPackageInfoFromPackageJson(srcPath, npmVersion)
	if err != nil {
		return nil, err
	}
	name := packageInfo.BuildInfoModuleId()

	return &NpmModule{name: name, srcPath: srcPath, containingBuild: containingBuild, executablePath: executablePath}, nil
}

func (nm *NpmModule) CalcDependencies() error {
	if !nm.containingBuild.buildNameAndNumberProvided() {
		return errors.New("a build name must be provided in order to collect the project's dependencies")
	}
	buildInfoDependencies, err := buildutils.CalculateNpmDependenciesList(nm.executablePath, nm.srcPath, nm.name, nm.npmArgs, true, nm.containingBuild.logger)
	if err != nil {
		return err
	}
	buildInfoModule := entities.Module{Id: nm.name, Type: entities.Npm, Dependencies: buildInfoDependencies}
	buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}
	return nm.containingBuild.SaveBuildInfo(buildInfo)
}

func (nm *NpmModule) SetName(name string) {
	nm.name = name
}

func (nm *NpmModule) SetNpmArgs(npmArgs []string) {
	nm.npmArgs = npmArgs
}

func (nm *NpmModule) AddArtifacts(artifacts ...entities.Artifact) error {
	if !nm.containingBuild.buildNameAndNumberProvided() {
		return errors.New("a build name must be provided in order to add artifacts")
	}
	partial := &entities.Partial{ModuleId: nm.name, ModuleType: entities.Npm, Artifacts: artifacts}
	return nm.containingBuild.SavePartialBuildInfo(partial)
}
