Initial commit.
This commit is contained in:
commit
a7bf1f29ba
23
README.md
Normal file
23
README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# GLP
|
||||||
|
|
||||||
|
GLP stands for Go Licensing Processor. It is an utility that parses application's dependencies, gets their licenses and copyright information and writing gathered data into report file.
|
||||||
|
|
||||||
|
## Supported report file formats
|
||||||
|
|
||||||
|
*None yet.*
|
||||||
|
|
||||||
|
## Supported VCSes and sites
|
||||||
|
|
||||||
|
*None yet.*
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
It is enough to issue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u go.dev.pztrn.name/glp/cmd/glp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
*None yet.*
|
53
cmd/glp/main.go
Normal file
53
cmd/glp/main.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
"go.dev.pztrn.name/glp/httpclient"
|
||||||
|
"go.dev.pztrn.name/glp/outputters"
|
||||||
|
"go.dev.pztrn.name/glp/parsers"
|
||||||
|
"go.dev.pztrn.name/glp/projecter"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configurationPath string
|
||||||
|
packagesPaths string
|
||||||
|
outputFormat string
|
||||||
|
outputFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Starting glp")
|
||||||
|
|
||||||
|
flag.StringVar(&configurationPath, "config", "./.glp.yaml", "Path to configuration file.")
|
||||||
|
flag.StringVar(&packagesPaths, "pkgs", "", "Packages that should be analyzed. Use comma to delimit packages.")
|
||||||
|
flag.StringVar(&outputFormat, "outformat", "csv", "Output file format. Only 'csv' for now.")
|
||||||
|
flag.StringVar(&outputFile, "outfile", "", "File to write licensing information to.")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if packagesPaths == "" {
|
||||||
|
log.Println("Packages paths that should be analyzed should be defined.")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputFile == "" {
|
||||||
|
log.Println("Output file path should be defined.")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.Initialize(configurationPath)
|
||||||
|
parsers.Initialize()
|
||||||
|
outputters.Initialize()
|
||||||
|
httpclient.Initialize()
|
||||||
|
|
||||||
|
projecter.Initialize(packagesPaths, outputFormat, outputFile)
|
||||||
|
projecter.Parse()
|
||||||
|
}
|
29
configuration/exported.go
Normal file
29
configuration/exported.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configurationPath string
|
||||||
|
|
||||||
|
Cfg *config
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes package.
|
||||||
|
func Initialize(cfgpath string) {
|
||||||
|
log.Println("Initializing configuration")
|
||||||
|
|
||||||
|
configurationPath = cfgpath
|
||||||
|
|
||||||
|
Cfg = &config{}
|
||||||
|
err := Cfg.initialize()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error appeared when loading configuration:", err.Error())
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
65
configuration/struct.go
Normal file
65
configuration/struct.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This structure holds whole configuration for glp.
|
||||||
|
type config struct {
|
||||||
|
Log struct {
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
} `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to parse configuration.
|
||||||
|
func (c *config) initialize() error {
|
||||||
|
// Check if file exists.
|
||||||
|
if _, err := os.Stat(configurationPath); os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration file path.
|
||||||
|
// First - replace any "~" that might appear.
|
||||||
|
if strings.Contains(configurationPath, "~") {
|
||||||
|
userDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationPath = strings.Replace(configurationPath, "~", userDir, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then - make relative paths to be absolute.
|
||||||
|
absPath, err1 := filepath.Abs(configurationPath)
|
||||||
|
if err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationPath = absPath
|
||||||
|
|
||||||
|
log.Println("Trying to load configuration file data from '" + configurationPath + "'")
|
||||||
|
|
||||||
|
// Read file into memory.
|
||||||
|
fileData, err2 := ioutil.ReadFile(configurationPath)
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse loaded data.
|
||||||
|
err3 := yaml.Unmarshal(fileData, c)
|
||||||
|
if err3 != nil {
|
||||||
|
return err3
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Log.Debug {
|
||||||
|
log.Printf("Configuration parsed: %+v\n", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
2
glp.example.yaml
Normal file
2
glp.example.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
log:
|
||||||
|
debug: true
|
122
httpclient/exported.go
Normal file
122
httpclient/exported.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package httpclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeoutInSeconds = 20
|
||||||
|
perDomainRequestsLimit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Timeout: time.Second * defaultTimeoutInSeconds,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: time.Second * defaultTimeoutInSeconds,
|
||||||
|
DualStack: true,
|
||||||
|
}).DialContext,
|
||||||
|
ExpectContinueTimeout: time.Second * 5,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ResponseHeaderTimeout: time.Second * defaultTimeoutInSeconds,
|
||||||
|
TLSHandshakeTimeout: time.Second * 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
perDomainRequests map[string]int
|
||||||
|
perDomainRequestsMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes package.
|
||||||
|
func Initialize() {
|
||||||
|
log.Println("Initializing HTTP client...")
|
||||||
|
|
||||||
|
perDomainRequests = make(map[string]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET executes GET request and returns body.
|
||||||
|
func GET(request *http.Request) []byte {
|
||||||
|
for {
|
||||||
|
perDomainRequestsMutex.Lock()
|
||||||
|
currentlyRunning, found := perDomainRequests[request.URL.Host]
|
||||||
|
perDomainRequestsMutex.Unlock()
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentlyRunning >= perDomainRequestsLimit {
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
perDomainRequestsMutex.Lock()
|
||||||
|
|
||||||
|
_, found := perDomainRequests[request.URL.Host]
|
||||||
|
if !found {
|
||||||
|
perDomainRequests[request.URL.Host] = 1
|
||||||
|
} else {
|
||||||
|
perDomainRequests[request.URL.Host]++
|
||||||
|
}
|
||||||
|
|
||||||
|
perDomainRequestsMutex.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
perDomainRequestsMutex.Lock()
|
||||||
|
|
||||||
|
perDomainRequests[request.URL.Host]--
|
||||||
|
|
||||||
|
perDomainRequestsMutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Println("Executing request:", request.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestsCount = 0
|
||||||
|
response *http.Response
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if requestsCount == 3 {
|
||||||
|
log.Printf("Failed to execute request %s: tried 3 times and got errors. Skipping.", request.URL.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
response, err = httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to execute request %s: %s\n", request.URL.String(), err.Error())
|
||||||
|
requestsCount++
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err1 := ioutil.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
|
if err1 != nil {
|
||||||
|
log.Printf("Failed to read response body %s: %s\n", request.URL.String(), err1.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody
|
||||||
|
}
|
49
outputters/csv/csv.go
Normal file
49
outputters/csv/csv.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package csv
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
c "encoding/csv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
headers = []string{"Module", "License", "Repository URL", "License URL", "Project", "Copyrights"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Responsible for pushing passed data into CSV file.
|
||||||
|
type outputter struct{}
|
||||||
|
|
||||||
|
func (o *outputter) Write(deps []*structs.Dependency, outFile string) {
|
||||||
|
log.Println("Got", strconv.Itoa(len(deps)), "dependencies to write")
|
||||||
|
|
||||||
|
// Check if file exists and remove it if so.
|
||||||
|
if _, err := os.Stat(outFile); !os.IsNotExist(err) || err == nil {
|
||||||
|
os.Remove(outFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file and create writer.
|
||||||
|
f, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to open '"+outFile+"' for writing:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := c.NewWriter(f)
|
||||||
|
writer.Comma = ';'
|
||||||
|
|
||||||
|
// Write header first.
|
||||||
|
_ = writer.Write(headers)
|
||||||
|
|
||||||
|
// Write dependencies information.
|
||||||
|
for _, dep := range deps {
|
||||||
|
_ = writer.Write([]string{dep.Name, dep.License.Name, dep.VCS.VCSPath, dep.License.URL, dep.Parent, dep.License.Copyrights})
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
}
|
16
outputters/csv/exported.go
Normal file
16
outputters/csv/exported.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package csv
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/outputters/outputinterface"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Initialize() outputinterface.Interface {
|
||||||
|
log.Println("Initializing csv outputter...")
|
||||||
|
|
||||||
|
c := &outputter{}
|
||||||
|
return outputinterface.Interface(c)
|
||||||
|
}
|
34
outputters/exported.go
Normal file
34
outputters/exported.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package outputters
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/outputters/csv"
|
||||||
|
"go.dev.pztrn.name/glp/outputters/outputinterface"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
outputters map[string]outputinterface.Interface
|
||||||
|
)
|
||||||
|
|
||||||
|
func Initialize() {
|
||||||
|
log.Println("Initializing output providers")
|
||||||
|
|
||||||
|
outputters = make(map[string]outputinterface.Interface)
|
||||||
|
|
||||||
|
csvIface := csv.Initialize()
|
||||||
|
outputters["csv"] = csvIface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write pushes parsed data into outputter for writing.
|
||||||
|
func Write(outputter string, filePath string, deps []*structs.Dependency) {
|
||||||
|
outputterIface, found := outputters[outputter]
|
||||||
|
if !found {
|
||||||
|
log.Fatalln("Failed to find outputter '" + outputter + "'!")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputterIface.Write(deps, filePath)
|
||||||
|
}
|
11
outputters/outputinterface/outputinterface.go
Normal file
11
outputters/outputinterface/outputinterface.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package outputinterface
|
||||||
|
|
||||||
|
import (
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface is a generic output writer interface.
|
||||||
|
type Interface interface {
|
||||||
|
Write(deps []*structs.Dependency, outFile string)
|
||||||
|
}
|
66
parsers/exported.go
Normal file
66
parsers/exported.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
"go.dev.pztrn.name/glp/parsers/golang"
|
||||||
|
"go.dev.pztrn.name/glp/parsers/parserinterface"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
parsers map[string]parserinterface.Interface
|
||||||
|
parsersMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes package.
|
||||||
|
func Initialize() {
|
||||||
|
log.Println("Initializing parsers...")
|
||||||
|
|
||||||
|
parsers = make(map[string]parserinterface.Interface)
|
||||||
|
|
||||||
|
// Initialize parsers.
|
||||||
|
golangIface, golangName := golang.Initialize()
|
||||||
|
parsers[golangName] = golangIface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect tries to launch parsers for project detection. It returns
|
||||||
|
// parser name that should be used and optional flavor (e.g. dependencies
|
||||||
|
// manager name) that might be returned by parser's Detect() function.
|
||||||
|
func Detect(pkgPath string) (string, string) {
|
||||||
|
parsersMutex.RLock()
|
||||||
|
defer parsersMutex.RUnlock()
|
||||||
|
|
||||||
|
for parserName, parserIface := range parsers {
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Println("Checking if parser '" + parserName + "' can parse project '" + pkgPath + "'...")
|
||||||
|
}
|
||||||
|
|
||||||
|
useThisParser, flavor := parserIface.Detect(pkgPath)
|
||||||
|
if useThisParser {
|
||||||
|
return parserName, flavor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDependencies asks parser to extract dependencies from project.
|
||||||
|
func GetDependencies(parserName string, flavor string, pkgPath string) ([]*structs.Dependency, error) {
|
||||||
|
parsersMutex.RLock()
|
||||||
|
defer parsersMutex.RUnlock()
|
||||||
|
parser, found := parsers[parserName]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, errors.New("parser with such name isn't registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
deps := parser.GetDependencies(flavor, pkgPath)
|
||||||
|
|
||||||
|
return deps, nil
|
||||||
|
}
|
109
parsers/golang/dep.go
Normal file
109
parsers/golang/dep.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
|
||||||
|
// other
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var goDepFilesToCheck = []string{"Gopkg.toml", "Gopkg.lock"}
|
||||||
|
|
||||||
|
type depLockConfig struct {
|
||||||
|
Projects []struct {
|
||||||
|
Branch string
|
||||||
|
Digest string
|
||||||
|
Name string
|
||||||
|
Packages []string
|
||||||
|
PruneOpts string
|
||||||
|
Revision string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
SolveMeta struct {
|
||||||
|
AnalyzerName string `toml:"analyzer-name"`
|
||||||
|
AnalyzerVersion int `toml:"analyzer-version"`
|
||||||
|
InputImports []string `toml:"input-imports"`
|
||||||
|
SolverName string `toml:"solver-name"`
|
||||||
|
SolverVersion int `toml:"solver-version"`
|
||||||
|
} `toml:"solve-meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detects if project is using dep for dependencies management.
|
||||||
|
func (gp *golangParser) detectDepUsage(pkgPath string) bool {
|
||||||
|
var goDepFilesFound bool
|
||||||
|
for _, fileName := range goDepFilesToCheck {
|
||||||
|
pathToCheck := filepath.Join(pkgPath, fileName)
|
||||||
|
if _, err := os.Stat(pathToCheck); err == nil {
|
||||||
|
goDepFilesFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if goDepFilesFound {
|
||||||
|
log.Println("Project '" + pkgPath + "' is using dep for dependencies management")
|
||||||
|
}
|
||||||
|
|
||||||
|
return goDepFilesFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets dependencies data from dep-enabled projects.
|
||||||
|
func (gp *golangParser) getDependenciesFromDep(pkgPath string) []*structs.Dependency {
|
||||||
|
deps := make([]*structs.Dependency, 0)
|
||||||
|
|
||||||
|
// Try to figure out parent package name for all dependencies.
|
||||||
|
parent := gp.getParentForDep(pkgPath)
|
||||||
|
|
||||||
|
// All dependencies for project will be taken from Gopkg.lock file.
|
||||||
|
lockFile := &depLockConfig{}
|
||||||
|
_, err := toml.DecodeFile(filepath.Join(pkgPath, "Gopkg.lock"), lockFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to parse dep lock file:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Printf("dep lock file parsed: %+v\n", lockFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse dependencies.
|
||||||
|
for _, dep := range lockFile.Projects {
|
||||||
|
dependency := &structs.Dependency{
|
||||||
|
Name: dep.Name,
|
||||||
|
Parent: parent,
|
||||||
|
VCS: structs.VCSData{
|
||||||
|
Branch: dep.Branch,
|
||||||
|
Revision: dep.Revision,
|
||||||
|
},
|
||||||
|
Version: dep.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If branch is empty - assume master.
|
||||||
|
if dependency.VCS.Branch == "" {
|
||||||
|
dependency.VCS.Branch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
deps = append(deps, dependency)
|
||||||
|
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Printf("Initial dependency structure formed: %+v\n", dependency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to get package name for passed package path.
|
||||||
|
func (gp *golangParser) getParentForDep(pkgPath string) string {
|
||||||
|
// Dep-managed projects are in 99% of cases are placed in GOPATH.
|
||||||
|
if strings.Contains(pkgPath, "src") {
|
||||||
|
return strings.Split(pkgPath, "src/")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
16
parsers/golang/exported.go
Normal file
16
parsers/golang/exported.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/parsers/parserinterface"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Initialize() (parserinterface.Interface, string) {
|
||||||
|
log.Println("Initializing Golang projects parser")
|
||||||
|
|
||||||
|
p := &golangParser{}
|
||||||
|
return parserinterface.Interface(p), "golang"
|
||||||
|
}
|
132
parsers/golang/godata.go
Normal file
132
parsers/golang/godata.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
"go.dev.pztrn.name/glp/httpclient"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gets go-import and go-source data and fill it in dependency.
|
||||||
|
func getGoData(dependency *structs.Dependency) {
|
||||||
|
// Dependencies are imported using URL which can be called with
|
||||||
|
// "?go-get=1" parameter to obtain required VCS data.
|
||||||
|
req, _ := http.NewRequest("GET", "http://"+dependency.Name, nil)
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("go-get", "1")
|
||||||
|
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
respBody := httpclient.GET(req)
|
||||||
|
if respBody == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML is hard to parse properly statically, so we will go
|
||||||
|
// line-by-line for <head> parsing.
|
||||||
|
resp := bytes.NewBuffer(respBody)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// This flag shows that we're currently parsing <head> from HTML.
|
||||||
|
headCurrentlyParsing bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := resp.ReadString('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
log.Fatalln("Failed to read HTML response line-by-line:", err.Error())
|
||||||
|
} else if err != nil && err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if headCurrentlyParsing {
|
||||||
|
// Check for go-import data.
|
||||||
|
if strings.Contains(line, `<meta name="go-import"`) {
|
||||||
|
// Get content.
|
||||||
|
// Import things are in element #4.
|
||||||
|
lineSplitted := strings.Split(line, `"`)
|
||||||
|
|
||||||
|
// Check line length. This is not so good approach, but
|
||||||
|
// should work for 99% of dependencies.
|
||||||
|
if len(lineSplitted) < 5 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - tag itself appears to be incomplete. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lineSplitted) > 5 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - line where meta tag is located appears to be too long. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import line contains data like VCS name and VCS URL.
|
||||||
|
// They're delimited with whitespace.
|
||||||
|
importDataSplitted := strings.Split(lineSplitted[3], " ")
|
||||||
|
|
||||||
|
// Import line should contain at least 3 elements.
|
||||||
|
if len(importDataSplitted) < 3 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - import data is too small. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill dependency data with this data.
|
||||||
|
// First element is a module name and we do not actually
|
||||||
|
// need it, because it is already filled previously.
|
||||||
|
dependency.VCS.VCS = importDataSplitted[1]
|
||||||
|
dependency.VCS.VCSPath = importDataSplitted[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for go-source data.
|
||||||
|
if strings.Contains(line, `<meta name="go-source"`) {
|
||||||
|
// Get content.
|
||||||
|
// Import things are in element #4.
|
||||||
|
lineSplitted := strings.Split(line, `"`)
|
||||||
|
|
||||||
|
// Check line length. This is not so good approach, but
|
||||||
|
// should work for 99% of dependencies.
|
||||||
|
if len(lineSplitted) < 5 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - tag itself appears to be incomplete. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lineSplitted) > 5 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - line where meta tag is located appears to be too long. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source line contains data like VCS paths templates.
|
||||||
|
// They're delimited with whitespace.
|
||||||
|
sourceDataSplitted := strings.Split(lineSplitted[3], " ")
|
||||||
|
|
||||||
|
// Source data line should contain at least 3 elements.
|
||||||
|
if len(sourceDataSplitted) < 4 {
|
||||||
|
log.Println("Got line: '" + line + "', but it cannot be parsed. Probably badly formed - source data is too small. Skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill dependency data.
|
||||||
|
dependency.VCS.SourceURLDirTemplate = sourceDataSplitted[2]
|
||||||
|
dependency.VCS.SourceURLFileTemplate = sourceDataSplitted[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(line), "<head>") {
|
||||||
|
headCurrentlyParsing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(line), "</head>") {
|
||||||
|
headCurrentlyParsing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Printf("go-import and go-source data parsed: %+v\n", dependency.VCS)
|
||||||
|
}
|
||||||
|
}
|
27
parsers/golang/modules.go
Normal file
27
parsers/golang/modules.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var goModulesFilesToCheck = []string{"go.mod", "go.sum"}
|
||||||
|
|
||||||
|
// Detects if project is using go modules for dependencies management.
|
||||||
|
func (gp *golangParser) detectModulesUsage(pkgPath string) bool {
|
||||||
|
var goModulesFileFound bool
|
||||||
|
for _, fileName := range goModulesFilesToCheck {
|
||||||
|
pathToCheck := filepath.Join(pkgPath, fileName)
|
||||||
|
if _, err := os.Stat(pathToCheck); err == nil {
|
||||||
|
goModulesFileFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if goModulesFileFound {
|
||||||
|
log.Println("Project '" + pkgPath + "' is using Go modules for dependencies management")
|
||||||
|
}
|
||||||
|
|
||||||
|
return goModulesFileFound
|
||||||
|
}
|
67
parsers/golang/parser.go
Normal file
67
parsers/golang/parser.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Package managers names. Used in Detect() for flavor returning.
|
||||||
|
packageManagerGoMod = "go mod"
|
||||||
|
packageManagerDep = "dep"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This structure responsible for parsing projects that written in Go.
|
||||||
|
type golangParser struct{}
|
||||||
|
|
||||||
|
// Detect detects if passed project path can be parsed with this parser
|
||||||
|
// and additionally detect package manager used.
|
||||||
|
func (gp *golangParser) Detect(pkgPath string) (bool, string) {
|
||||||
|
// Go projects usually using go modules or dep for dependencies
|
||||||
|
// management.
|
||||||
|
isModules := gp.detectModulesUsage(pkgPath)
|
||||||
|
if isModules {
|
||||||
|
return true, packageManagerGoMod
|
||||||
|
}
|
||||||
|
|
||||||
|
isDep := gp.detectDepUsage(pkgPath)
|
||||||
|
if isDep {
|
||||||
|
return true, packageManagerDep
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDependencies extracts dependencies from project.
|
||||||
|
func (gp *golangParser) GetDependencies(flavor string, pkgPath string) []*structs.Dependency {
|
||||||
|
var deps []*structs.Dependency
|
||||||
|
|
||||||
|
switch flavor {
|
||||||
|
case packageManagerDep:
|
||||||
|
deps = gp.getDependenciesFromDep(pkgPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return early if no dependencies was found.
|
||||||
|
if len(deps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every dependency we should get additional data - go-import
|
||||||
|
// and go-source. Asynchronously.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, dep := range deps {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(dep *structs.Dependency) {
|
||||||
|
getGoData(dep)
|
||||||
|
wg.Done()
|
||||||
|
}(dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return deps
|
||||||
|
}
|
16
parsers/parserinterface/parserinterface.go
Normal file
16
parsers/parserinterface/parserinterface.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package parserinterface
|
||||||
|
|
||||||
|
import (
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface is a generic parser interface.
|
||||||
|
type Interface interface {
|
||||||
|
// Detect should return true if project should be parsed using
|
||||||
|
// this parser and false otherwise. May optionally return package
|
||||||
|
// flavor (e.g. dependency management utility name).
|
||||||
|
Detect(pkgPath string) (bool, string)
|
||||||
|
// GetDependencies parses project for dependencies.
|
||||||
|
GetDependencies(flavor string, pkgPath string) []*structs.Dependency
|
||||||
|
}
|
88
projecter/exported.go
Normal file
88
projecter/exported.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package projecter
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/configuration"
|
||||||
|
"go.dev.pztrn.name/glp/outputters"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
packages []string
|
||||||
|
|
||||||
|
outputFormat string
|
||||||
|
outputFile string
|
||||||
|
|
||||||
|
projects map[string]*Project
|
||||||
|
projectsMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes package.
|
||||||
|
func Initialize(pkgs string, outFormat string, outFile string) {
|
||||||
|
log.Println("Initializing projects handler...")
|
||||||
|
|
||||||
|
packages = strings.Split(pkgs, ",")
|
||||||
|
projects = make(map[string]*Project)
|
||||||
|
|
||||||
|
outputFormat = outFormat
|
||||||
|
outputFile = outFile
|
||||||
|
|
||||||
|
log.Println("Packages list that was passed:", packages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject returns project by it's path.
|
||||||
|
func GetProject(path string) *Project {
|
||||||
|
projectsMutex.RLock()
|
||||||
|
defer projectsMutex.RUnlock()
|
||||||
|
|
||||||
|
prj, found := projects[path]
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return prj
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse starts projects parsing.
|
||||||
|
func Parse() {
|
||||||
|
// Create project for every passed package.
|
||||||
|
// This is done in main goroutine and therefore no mutex is used.
|
||||||
|
for _, pkgPath := range packages {
|
||||||
|
prj := NewProject(pkgPath)
|
||||||
|
projects[pkgPath] = prj
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.Cfg.Log.Debug {
|
||||||
|
log.Printf("Projects generated: %+v\n", projects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should start asynchronous projects parsing.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, prj := range projects {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(prj *Project) {
|
||||||
|
prj.process()
|
||||||
|
wg.Done()
|
||||||
|
}(prj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until all projects will be parsed.
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Collect dependencies list from all parsed projects.
|
||||||
|
var deps []*structs.Dependency
|
||||||
|
|
||||||
|
for _, prj := range projects {
|
||||||
|
deps = append(deps, prj.GetDeps()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputters.Write(outputFormat, outputFile, deps)
|
||||||
|
|
||||||
|
log.Println("Parsing done")
|
||||||
|
}
|
80
projecter/project.go
Normal file
80
projecter/project.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package projecter
|
||||||
|
|
||||||
|
import (
|
||||||
|
// stdlib
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// local
|
||||||
|
"go.dev.pztrn.name/glp/parsers"
|
||||||
|
"go.dev.pztrn.name/glp/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Project represents single project (or package) that was passed via
|
||||||
|
// -pkgs parameter.
|
||||||
|
type Project struct {
|
||||||
|
packagePath string
|
||||||
|
parserName string
|
||||||
|
flavor string
|
||||||
|
|
||||||
|
deps []*structs.Dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProject creates new project and returns it.
|
||||||
|
func NewProject(packagePath string) *Project {
|
||||||
|
p := &Project{}
|
||||||
|
p.initialize(packagePath)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeps returns list of dependencies for project.
|
||||||
|
func (p *Project) GetDeps() []*structs.Dependency {
|
||||||
|
return p.deps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes project.
|
||||||
|
func (p *Project) initialize(packagePath string) {
|
||||||
|
p.packagePath = packagePath
|
||||||
|
|
||||||
|
// Prepare package path to be used.
|
||||||
|
// First - replace "~" with actual home directory.
|
||||||
|
if strings.Contains(p.packagePath, "~") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to get user's home directory:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.packagePath = strings.Replace(p.packagePath, "~", homeDir, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get absolute path.
|
||||||
|
var err error
|
||||||
|
p.packagePath, err = filepath.Abs(p.packagePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to get absolute path for package '"+p.packagePath+":", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts project parsing.
|
||||||
|
func (p *Project) process() {
|
||||||
|
// We should determine project type.
|
||||||
|
p.parserName, p.flavor = parsers.Detect(p.packagePath)
|
||||||
|
|
||||||
|
if p.parserName == "unknown" {
|
||||||
|
log.Println("Project", p.packagePath, "cannot be parsed with glp")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets try to get dependencies, their versions and URLs.
|
||||||
|
deps, err := parsers.GetDependencies(p.parserName, p.flavor, p.packagePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to get dependencies:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.deps = deps
|
||||||
|
|
||||||
|
// ToDo: get licensing information.
|
||||||
|
}
|
18
structs/dependency.go
Normal file
18
structs/dependency.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
// Dependency represents single dependency data.
|
||||||
|
type Dependency struct {
|
||||||
|
// License is a license name for dependency.
|
||||||
|
License License
|
||||||
|
// Name is a dependency name as it appears in package manager's
|
||||||
|
// lock file or in sources if no package manager is used.
|
||||||
|
Name string
|
||||||
|
// Parent is a path to parent package.
|
||||||
|
Parent string
|
||||||
|
// VCS is a VCS data obtained for dependency.
|
||||||
|
VCS VCSData
|
||||||
|
// Version is a dependency version used in project.
|
||||||
|
Version string
|
||||||
|
// URL is a web URL for that dependency (Github, Gitlab, etc.).
|
||||||
|
URL string
|
||||||
|
}
|
7
structs/license.go
Normal file
7
structs/license.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
type License struct {
|
||||||
|
Copyrights string
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
}
|
19
structs/vcsdata.go
Normal file
19
structs/vcsdata.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
// VCSData describes structure of go-import and go-source data.
|
||||||
|
type VCSData struct {
|
||||||
|
// Branch is a VCS branch used.
|
||||||
|
Branch string
|
||||||
|
// Revision is a VCS revision used.
|
||||||
|
Revision string
|
||||||
|
// SourceURLDirTemplate is a template for sources dirs URLs. E.g.:
|
||||||
|
// https://sources.dev.pztrn.name/pztrn/glp/src/branch/master{/dir}
|
||||||
|
SourceURLDirTemplate string
|
||||||
|
// SourceURLFileTemplate is a template for sources files URLs. E.g.:
|
||||||
|
// https://sources.dev.pztrn.name/pztrn/glp/src/branch/master{/dir}/{file}#L{line}
|
||||||
|
SourceURLFileTemplate string
|
||||||
|
// VCS is a VCS name (e.g. "git").
|
||||||
|
VCS string
|
||||||
|
// VCSPath is a VCS repository path.
|
||||||
|
VCSPath string
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user