diff --git a/README.md b/README.md index e1b94206..18a1e1a5 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,12 @@ bi dotnet [Dotnet command] [command options] bi nuget [Nuget command] [command options] ``` +#### Cargo + +```shell +bi cargo [Cargo command] [command options] +``` + #### Conversion to CycloneDX You can generate build-info and have it converted into the CycloneDX format by adding to the @@ -527,6 +533,15 @@ nugetModule, err := bld.AddNugetModules(nugetProjectPath) err = nugetModule.CalcDependencies() ``` +#### Cargo + +```go +// You can pass an empty string as an argument, if the root of the Cargo project is the working directory. +cargoModule, err := bld.AddCargoModules(cargoProjectPath) +// Calculate the dependencies used by this module, and store them in the module struct. +err = cargoModule.CalcDependencies() +``` + ### Collecting Environment Variables Using `CollectEnv()` you can collect environment variables and attach them to the build. diff --git a/build-info.json b/build-info.json new file mode 100644 index 00000000..3cc6f706 --- /dev/null +++ b/build-info.json @@ -0,0 +1,351 @@ +{ + "name": "go-build", + "number": "1", + "agent": {}, + "buildAgent": { + "name": "GENERIC" + }, + "modules": [ + { + "type": "go", + "id": "github.com/jfrog/build-info-go", + "dependencies": [ + { + "id": "github.com/buger/jsonparser:v1.1.1", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "e0c54d96564262a70bc7ed33fb3ee2b15596f68f", + "md5": "7ab77d10951f73b96b9c19a6cca51bb1", + "sha256": "be17ef1b44c22eac645eeac80f0e26cdfc70d77262e631358e00c2aa817eab8c" + }, + { + "id": "github.com/urfave/cli/v2:v2.25.7", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "c93f96a6feceef906da4bdb4d337ad5fa9e5048d", + "md5": "b5bbdbfb52f8f83b451200a216b6d504", + "sha256": "10941b24689d7c953a78b6d196a03e04274e012d27d4f297fe844cf58221a86c" + }, + { + "id": "github.com/cpuguy83/go-md2man/v2:v2.0.2", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "cab8c09415bb3aa49892aac866a2732980d44c95", + "md5": "86a066c9aaad807da54f481b8286f8ce", + "sha256": "70a7e609809cf2a92c5535104db5eb82d75c54bfcfed2d224e87dd2fd9729f62" + }, + { + "id": "github.com/minio/sha256-simd:v1.0.1", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "c6b381b6f945ddea88df93edebf185f29c7fc477", + "md5": "89452597fdd0efbda45051a98284f8cd", + "sha256": "e8805d8f0438b7fa0286c0cb160180ad8fc726e06bca1eabcd59c142523c625c" + }, + { + "id": "github.com/jfrog/gofrog:v1.4.1", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "0461da8f5992650adfed08958b6128ccf762ad19", + "md5": "b99ca543f9b6f8978e9e7013c5154493", + "sha256": "2d527597437427ca2b1ca1da5d29123cfaf0b64a916565647b788f7ee7ded5ab" + }, + { + "id": "github.com/xeipuuv/gojsonpointer:v0.0.0-20180127040702-4e3ac2762d5f", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/xeipuuv/gojsonschema:v1.2.0", + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/xeipuuv/gojsonschema:v1.2.0", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "c00ffab826fdd7e3aa1284ed3a0918cbdf2ec095", + "md5": "812aaf45e505b2953d31a75ce668e46e", + "sha256": "5b1a4bcc8e003f214c92b3fa52959d9eb0e3af1c0c529efa55815db951146e48" + }, + { + "id": "github.com/russross/blackfriday/v2:v2.1.0", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/cpuguy83/go-md2man/v2:v2.0.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/cpuguy83/go-md2man/v2:v2.0.2", + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "b733cda2c795193ad2a65e13dcd7529b93bf04e9", + "md5": "eea4411c54002a5fb7d0db351270eefe", + "sha256": "7852750d58a053ce38b01f2c203208817564f552ebf371b2b630081d7004c6ae" + }, + { + "id": "github.com/davecgh/go-spew:v1.1.1", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/gofrog:v1.4.1", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "0f9760bda0c6ccacac5e57f62d0f5ad9c7dab03f", + "md5": "feef6644bd69286382139b28be3f0b91", + "sha256": "6b44a843951f371b7010c754ecc3cabefe815d5ced1c5b9409fb2d697e8a890d" + }, + { + "id": "github.com/klauspost/cpuid/v2:v2.2.3", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/minio/sha256-simd:v1.0.1", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "6429461f86edd94bb679272748c522f41f6453df", + "md5": "1bbcae037201b315dccd6e535b20bca0", + "sha256": "f68ff82caa807940fee615b4898d428365761eeb36861959ca8b91a034bd0e7e" + }, + { + "id": "github.com/!burnt!sushi/toml:v1.3.2", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "a0fc876bb92fdbe9ad75c300588f2c63e954985c", + "md5": "e06df9631c9e1897912dfa92a40b4928", + "sha256": "5de246a0cb4c256f3fd5d0db8a08a114f58af0c2e193bbf0ad9012104adbb6b2" + }, + { + "id": "github.com/xrash/smetrics:v0.0.0-20201216005158-039620a65673", + "type": "zip", + "requestedBy": [ + [ + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "79a771d85bf5b4df5b583c8435343b323f52995e", + "md5": "b5a17afb05c11c10713e33e390729bd7", + "sha256": "bbebb9a00f44ff3e27bec16111effdcf2706d727821a4833ec8da19aad96e26d" + }, + { + "id": "github.com/!cyclone!d!x/cyclonedx-go:v0.7.2", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "ca3fae95bf1b898f1dff98f4ea7c2de175f4d4ff", + "md5": "e6c907f90de64339ee4d4222a3de7f1a", + "sha256": "0ec9a7c538af92e884d7caed88c62890d5eaadc3d0bb0e2e0e0d7c5d0cb5fdbb" + }, + { + "id": "github.com/xeipuuv/gojsonschema:v1.2.0", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "e1529901eb2cf8c9ff2b1fbf504cb08d05d3d578", + "md5": "ebbf84ea1a07065b100c33e2736e6d03", + "sha256": "55c8ce068257aa0d263aad7470113dafcd50f955ee754fc853c2fdcd31ad096f" + }, + { + "id": "github.com/xeipuuv/gojsonreference:v0.0.0-20180127040603-bd5ef7bd5415", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/xeipuuv/gojsonschema:v1.2.0", + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/xeipuuv/gojsonschema:v1.2.0", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "133e9c4987a455db1a748f79522b79e95bd395ff", + "md5": "1355152ef669012354342f3f0a133987", + "sha256": "7ec98f4df894413f4dc58c8df330ca8b24ff425b05a8e1074c3028c99f7e45e7" + }, + { + "id": "gopkg.in/yaml.v3:v3.0.1", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/gofrog:v1.4.1", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/urfave/cli/v2:v2.25.7", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "65825246447882d6f2bddb3f89ac4b9abc2612ef", + "md5": "292e318d64256fb05395c45c176c94c2", + "sha256": "aab8fbc4e6300ea08e6afe1caea18a21c90c79f489f52c53e2f20431f1a9a015" + }, + { + "id": "github.com/pmezard/go-difflib:v1.0.0", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/gofrog:v1.4.1", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/stretchr/testify:v1.8.4", + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "f200e2a5211b527ef2d2ff301718ccc4ad5c705b", + "md5": "fb72df530a7f3fca56ccc192c9f30a58", + "sha256": "de04cecc1a4b8d53e4357051026794bcbc54f2e6a260cfac508ce69d5d6457a0" + }, + { + "id": "github.com/stretchr/testify:v1.8.4", + "type": "zip", + "requestedBy": [ + [ + "github.com/CycloneDX/cyclonedx-go:v0.7.2", + "github.com/jfrog/build-info-go" + ], + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "874d98aac38304b0ac3e046475fb39f11219d5c0", + "md5": "7bbb2058ef66c6829ebd3b652aff5296", + "sha256": "e206daaede0bd03de060bdfbeb984ac2c49b83058753fffc93fe0c220ea87532" + }, + { + "id": "golang.org/x/exp:v0.0.0-20230905200255-921286631fa9", + "type": "zip", + "requestedBy": [ + [ + "github.com/jfrog/build-info-go" + ] + ], + "sha1": "34532361e43d59695caabf5248d0464aae3561b9", + "md5": "a351a01d700150d778eeae97bd6859d0", + "sha256": "e789921a203695edf0d1bffc98b0f76b5f9a264c2f88ef084a2ea9a768081f85" + } + ] + } + ], + "started": "2024-01-07T11:48:00.759+0000" +} diff --git a/build/build.go b/build/build.go index d6987db3..7fcd68f6 100644 --- a/build/build.go +++ b/build/build.go @@ -80,6 +80,11 @@ func (b *Build) AddGoModule(srcPath string) (*GoModule, error) { return newGoModule(srcPath, b) } +// AddCargoModule adds a Go module to this Build. Pass srcPath as an empty string if the root of the Cargo project is the working directory. +func (b *Build) AddCargoModule(srcPath string) (*CargoModule, error) { + return newCargoModule(srcPath, b) +} + // AddMavenModule adds a Maven module to this Build. Pass srcPath as an empty string if the root of the Maven project is the working directory. func (b *Build) AddMavenModule(srcPath string) (*MavenModule, error) { return newMavenModule(b, srcPath) diff --git a/build/cargo.go b/build/cargo.go new file mode 100644 index 00000000..78a96890 --- /dev/null +++ b/build/cargo.go @@ -0,0 +1,333 @@ +package build + +import ( + "errors" + "fmt" + "github.com/BurntSushi/toml" + "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/build-info-go/utils" + gofrogcmd "github.com/jfrog/gofrog/io" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +var cargoModuleFile = "Cargo.toml" + +type CargoModule struct { + containingBuild *Build + name string + version string + srcPath string +} + +func newCargoModule(srcPath string, containingBuild *Build) (*CargoModule, error) { + var err error + if srcPath == "" { + srcPath, err = getProjectRoot() + if err != nil { + return nil, err + } + } + + // Read module name + name, version, err := getModuleNameByDirWithVersion(srcPath) + if err != nil { + return nil, err + } + return &CargoModule{name: name, version: version, srcPath: srcPath, containingBuild: containingBuild}, nil +} +func (gm *CargoModule) SetName(name string) { + gm.name = name +} +func getProjectRoot() (string, error) { + // Get the current directory. + wd, err := os.Getwd() + if err != nil { + return wd, err + } + return utils.FindFileInDirAndParents(wd, cargoModuleFile) +} + +func getModuleNameByDirWithVersion(projectDir string) (string, string, error) { + var values map[string]map[string]string + + _, err := toml.DecodeFile(path.Join(projectDir, cargoModuleFile), &values) + return values["package"]["name"], values["package"]["version"], err +} +func (gm *CargoModule) 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 + ":" + gm.version, ModuleType: entities.Cargo, Artifacts: artifacts} + return gm.containingBuild.SavePartialBuildInfo(partial) +} +func (cm *CargoModule) CalcDependencies() error { + if !cm.containingBuild.buildNameAndNumberProvided() { + return errors.New("a build name must be provided in order to collect the project's dependencies") + } + buildInfoDependencies, err := cm.loadDependencies() + if err != nil { + return err + } + + buildInfoModule := entities.Module{Id: cm.name + ":" + cm.version, Type: entities.Cargo, Dependencies: buildInfoDependencies} + buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}} + + return cm.containingBuild.SaveBuildInfo(buildInfo) +} + +func (cm *CargoModule) loadDependencies() ([]entities.Dependency, error) { + cachePath, err := getCachePath(cm.containingBuild.logger) + if err != nil { + return nil, err + } + dependenciesGraph, err := getDependenciesGraph(cm.srcPath, cm.containingBuild.logger) + if err != nil { + return nil, err + } + mapOfDeps := map[string]bool{} + for k, v := range dependenciesGraph { + mapOfDeps[k] = true + for _, v2 := range v { + mapOfDeps[v2] = true + } + } + dependenciesMap, err := cm.getCargoDependencies(cachePath, mapOfDeps) + if err != nil { + return nil, err + } + emptyRequestedBy := [][]string{{}} + populateRequestedByField(cm.name+":"+cm.version, emptyRequestedBy, dependenciesMap, dependenciesGraph) + return dependenciesMapToList(dependenciesMap), nil +} +func (cm *CargoModule) GetCargoMainArtifactAfterPublish() (artifact entities.Artifact, err error) { + wd, err := getProjectRoot() + if err != nil { + return + } + path := path.Join(wd, "target", "package", cm.name+"-"+cm.version+".crate") + artifact, err = populateArtifact(cm.name+":"+cm.version, path) + return +} +func populateArtifact(packageId, zipPath string) (artifact entities.Artifact, err error) { + artifact = entities.Artifact{Name: packageId} + md5, sha1, sha2, err := utils.GetFileChecksums(zipPath) + if err != nil { + return + } + artifact.Type = "cargo" + artifact.Checksum = entities.Checksum{Sha1: sha1, Md5: md5, Sha256: sha2} + return +} +func (cm *CargoModule) getCargoDependencies(cachePath string, depList map[string]bool) (map[string]entities.Dependency, error) { + // Create a map from dependency to parents + buildInfoDependencies := make(map[string]entities.Dependency) + for moduleId := range depList { + zipPath, err := getCrateLocation(cachePath, moduleId, cm.containingBuild.logger) + if err != nil { + return nil, err + } + if zipPath == "" { + continue + } + zipDependency, err := populateCrate(moduleId, zipPath) + if err != nil { + return nil, err + } + buildInfoDependencies[moduleId] = zipDependency + } + return buildInfoDependencies, nil +} + +func getDependenciesGraph(projectDir string, log utils.Log) (map[string][]string, error) { + cmdArgs := []string{"tree", "--prefix=depth"} + output, err := runDependenciesCmd(projectDir, cmdArgs, log) + if err != nil { + return nil, err + } + return graphToMap(output), err +} +func populateCrate(packageId, zipPath string) (zipDependency entities.Dependency, err error) { + zipDependency = entities.Dependency{Id: packageId} + md5, sha1, sha2, err := utils.GetFileChecksums(zipPath) + if err != nil { + return + } + zipDependency.Type = "cargo" + zipDependency.Checksum = entities.Checksum{Sha1: sha1, Md5: md5, Sha256: sha2} + return +} + +var deplineRegex = regexp.MustCompile(`([0-9]+)(\S+) v(\S+)( \(.*\))?`) + +func graphToMap(output string) map[string][]string { + var stack Stack + lineOutput := strings.Split(output, "\n") + mapOfDeps := map[string][]string{} + prevDepth := -1 + currParent := "" + prevLineName := "" + for _, line := range lineOutput { + if line == "" { + continue + } + // The expected syntax : {depth}name v{ver} (features) + // e.g.: 4proc-macro-error-attr v1.0.4 (proc-macro) + match := deplineRegex.FindStringSubmatch(line) + //// parse + depth, err := strconv.Atoi(match[1]) + if err != nil { + panic(err) + } + nameAndVersion := match[2] + ":" + match[3] + + switch { + case depth == prevDepth: + // no need to change parent or if 0, no need to store at all + case depth > prevDepth: + stack.Push(currParent) + currParent = prevLineName + default: + diff := prevDepth - depth // how many parents to pop + for i := 0; i < diff; i++ { + currParent, _ = stack.Pop() + } + } + if depth > 0 { + mapOfDeps[currParent] = append(mapOfDeps[currParent], nameAndVersion) + } else { + prevLineName = nameAndVersion + prevDepth = depth + continue + } + prevDepth = depth + prevLineName = nameAndVersion + } + return mapOfDeps +} + +func getCargoHome(log utils.Log) (cargoHome string, err error) { + // check for env var CARGO_HOME treat both abs and relative values + // if fails, check for location of binary + log.Debug("Searching for Cargo home.") + cargoHome = os.Getenv("CARGO_HOME") + if cargoHome == "" { + cargo, err := exec.LookPath("cargo") + if err != nil { + return "", err + } + cargoHome = path.Dir(path.Dir(cargo)) + } else if !path.IsAbs(cargoHome) { + // for relative path, prepend current directory to it + wd, err := os.Getwd() + if err != nil { + return wd, err + } + cargoHome = filepath.Join(wd, cargoHome) + } + + log.Debug("Cargo home location:", cargoHome) + + return +} + +func getCachePath(log utils.Log) (string, error) { + cargoHome, err := getCargoHome(log) + if err != nil { + return "", err + } + return filepath.Join(cargoHome, "registry", "cache"), nil +} +func getCrateLocation(cachePath, encodedDependencyId string, log utils.Log) (cratePath string, err error) { + moduleInfo := strings.Split(encodedDependencyId, ":") + if len(moduleInfo) != 2 { + log.Debug("The encoded dependency Id syntax should be 'name:version' but instead got:", encodedDependencyId) + return "", nil + } + dependencyName := moduleInfo[0] + version := moduleInfo[1] + entries, err := os.ReadDir(cachePath) + if err != nil { + return "", fmt.Errorf("could not read cache directory. %s", err) + } + fileExists := false + for _, file := range entries { + if file.IsDir() { + cratePath = filepath.Join(cachePath, file.Name(), dependencyName+"-"+version+".crate") + fileExists, err = utils.IsFileExists(cratePath, true) + if err != nil { + return "", fmt.Errorf("could not find zip binary for dependency '%s' at %s: %s", dependencyName, cratePath, err) + } + if fileExists { + break + } + } + } + + // Crate binary does not exist, so we skip it by returning a nil dependency. + if !fileExists { + log.Debug("Could not find crate") + return "", nil + } + return cratePath, nil +} + +type Stack []string + +func (s *Stack) Push(str string) { + *s = append(*s, str) +} +func (s *Stack) Pop() (string, bool) { + if len(*s) == 0 { + return "", false + } else { + index := len(*s) - 1 + element := (*s)[index] + *s = (*s)[:index] + return element, true + } +} + +func runDependenciesCmd(projectDir string, commandArgs []string, log utils.Log) (output string, err error) { + log.Info(fmt.Sprintf("Running 'cargo %s' in %s", strings.Join(commandArgs, " "), projectDir)) + if projectDir == "" { + projectDir, err = getProjectRoot() + if err != nil { + return "", err + } + } + + goCmd := gofrogcmd.NewCommand("cargo", "", commandArgs) + goCmd.Dir = projectDir + + /// err = prepareGlobalRegExp() + /// if err != nil { + /// return "", err + /// } + /// performPasswordMask, err := shouldMaskPassword() + /// if err != nil { + /// return "", err + /// } + var executionError error + var errorOut string + /// if performPasswordMask { + /// output, errorOut, _, executionError = gofrogcmd.RunCmdWithOutputParser(goCmd, false, protocolRegExp) + /// } else { + output, errorOut, _, executionError = gofrogcmd.RunCmdWithOutputParser(goCmd, false) + /// } + if len(output) != 0 { + log.Debug(output) + } + if executionError != nil { + // If the command fails, the mod stays the same, therefore, don't need to be restored. + errorString := fmt.Sprintf("Failed running Cargo command: 'cargo %s' in %s with error: '%s - %s'", strings.Join(commandArgs, " "), projectDir, executionError.Error(), errorOut) + return "", errors.New(errorString) + } + + return output, err +} diff --git a/build/cargo_test.go b/build/cargo_test.go new file mode 100644 index 00000000..fc28d2cc --- /dev/null +++ b/build/cargo_test.go @@ -0,0 +1,59 @@ +package build + +import ( + "github.com/jfrog/build-info-go/utils" + "path/filepath" + "testing" + + "github.com/jfrog/build-info-go/entities" + "github.com/stretchr/testify/assert" +) + +func TestGenerateBuildInfoForCargoProject(t *testing.T) { + if utils.IsWindows() { + return + } + service := NewBuildInfoService() + cargoBuild, err := service.GetOrCreateBuild("build-info-go-test-cargo4", "5") + assert.NoError(t, err) + defer func() { + assert.NoError(t, cargoBuild.Clean()) + }() + cargoModule, err := cargoBuild.AddCargoModule(filepath.Join("testdata", "cargo", "project")) + if assert.NoError(t, err) { + err = cargoModule.CalcDependencies() + assert.NoError(t, err) + err = cargoModule.AddArtifacts(entities.Artifact{Name: "artifactName", Type: "artifactType", Path: "artifactPath", Checksum: entities.Checksum{Sha1: "123", Md5: "456", Sha256: "789"}}) + assert.NoError(t, err) + buildInfo, err := cargoBuild.ToBuildInfo() + assert.NoError(t, err) + assert.Len(t, buildInfo.Modules, 1) + validateModule(t, buildInfo.Modules[0], 6, 1, "jfrog-dependency:0.0.2", entities.Cargo, true) + validateRequestedByCargo(t, buildInfo.Modules[0]) + } +} +func validateRequestedByCargo(t *testing.T, module entities.Module) { + for _, dep := range module.Dependencies { + if assert.NotEmpty(t, dep.RequestedBy, dep.Id+" RequestedBy field is empty") { + switch dep.Id { + // Direct dependencies: + case "want:0.3.1", "http:0.2.11": + assert.Equal(t, [][]string{{module.Id}}, dep.RequestedBy) + + // Indirect dependencies: + case "try-lock:0.2.5": + assert.Equal(t, [][]string{{"want:0.3.1", module.Id}}, dep.RequestedBy) + + case "itoa:1.0.10": + assert.Equal(t, [][]string{{"http:0.2.11", module.Id}}, dep.RequestedBy) + case "bytes:1.5.0": + assert.Equal(t, [][]string{{"http:0.2.11", module.Id}}, dep.RequestedBy) + case "fnv:1.0.7": + assert.Equal(t, [][]string{{"http:0.2.11", module.Id}}, dep.RequestedBy) + + default: + assert.Fail(t, "Unexpected dependency "+dep.Id) + } + } + } +} diff --git a/build/testdata/cargo/project/Cargo.lock b/build/testdata/cargo/project/Cargo.lock new file mode 100644 index 00000000..d5c736a8 --- /dev/null +++ b/build/testdata/cargo/project/Cargo.lock @@ -0,0 +1,55 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jfrog-dependency" +version = "0.0.2" +dependencies = [ + "http", + "want", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] diff --git a/build/testdata/cargo/project/Cargo.toml b/build/testdata/cargo/project/Cargo.toml new file mode 100644 index 00000000..7f14ebfb --- /dev/null +++ b/build/testdata/cargo/project/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jfrog-dependency" +version = "0.0.2" +description = "Dummy test project" + +[[bin]] +name = "jfrog-dependency" + +[dependencies] +want = "0.3.1" +http = "0.2.10" \ No newline at end of file diff --git a/build/testdata/cargo/project/src/main.rs b/build/testdata/cargo/project/src/main.rs new file mode 100644 index 00000000..d1c0d5c3 --- /dev/null +++ b/build/testdata/cargo/project/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + + println!("Hello World!"); +} \ No newline at end of file diff --git a/buildinfoschema_test.go b/buildinfoschema_test.go index df6493a5..d7286c69 100644 --- a/buildinfoschema_test.go +++ b/buildinfoschema_test.go @@ -17,6 +17,12 @@ func TestGoSchema(t *testing.T) { validateBuildInfoSchema(t, "go", filepath.Join("golang", "project"), func() {}) } +func TestCargoSchema(t *testing.T) { + if !utils.IsWindows() { + validateBuildInfoSchema(t, "cargo", filepath.Join("cargo", "project"), func() {}) + } +} + func TestMavenSchema(t *testing.T) { validateBuildInfoSchema(t, "mvn", filepath.Join("maven", "project"), func() {}) } diff --git a/cli/cli.go b/cli/cli.go index 1137164b..0b96a941 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -60,6 +60,36 @@ func GetCommands(logger utils.Log) []*clitool.Command { return printBuild(bld, context.String(formatFlag)) }, }, + { + Name: "cargo", + Usage: "Generate build-info for a Cargo project", + UsageText: "bi cargo", + Flags: flags, + Action: func(context *clitool.Context) (err error) { + service := build.NewBuildInfoService() + service.SetLogger(logger) + bld, err := service.GetOrCreateBuild("cargo-build", "1") + if err != nil { + return + } + defer func() { + e := bld.Clean() + if err == nil { + err = e + } + }() + cargoModule, err := bld.AddCargoModule("") + if err != nil { + return + } + + err = cargoModule.CalcDependencies() + if err != nil { + return + } + return printBuild(bld, context.String(formatFlag)) + }, + }, { Name: "mvn", Usage: "Generate build-info for a Maven project", diff --git a/entities/buildinfo.go b/entities/buildinfo.go index c2dc31af..85ae9344 100644 --- a/entities/buildinfo.go +++ b/entities/buildinfo.go @@ -33,6 +33,7 @@ const ( Go ModuleType = "go" Python ModuleType = "python" Terraform ModuleType = "terraform" + Cargo ModuleType = "cargo" ) type BuildInfo struct {