171 lines
3.2 KiB
Go
171 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
)
|
|
|
|
//go:embed "templates/deps.dot.tmpl"
|
|
var tpl string
|
|
|
|
// ComposerName is the reserved name of the Composer specifications file.
|
|
const ComposerName = "composer.json"
|
|
|
|
/*
|
|
In most cases: <owner>/<project>, but some special cases are valid too, like
|
|
"php", "ext-<extension>", "lib-<library>"
|
|
*/
|
|
type projectID string
|
|
type semverClause string
|
|
|
|
type node struct {
|
|
ID projectID
|
|
Path string
|
|
Links map[projectID]semverClause
|
|
}
|
|
|
|
type composer struct {
|
|
ID projectID `json:"name"`
|
|
Require map[projectID]semverClause
|
|
RequireDev map[projectID]semverClause
|
|
}
|
|
|
|
func (n *node) SetRequire(reqs map[projectID]semverClause) {
|
|
// todo Probably use a positive list instead of a negative list
|
|
parts := []string{
|
|
`api-platform/`,
|
|
`aporat/`,
|
|
`babdev/`,
|
|
`cocur/`,
|
|
`composer/`,
|
|
`doctrine/`,
|
|
`eo/`,
|
|
`ergebnis/`,
|
|
`evence/`,
|
|
`ext-`,
|
|
`firebase/`,
|
|
`flow/`,
|
|
`friendsofphp/`,
|
|
`friendsofsymfony/`,
|
|
`google/`,
|
|
`guzzlehttp/`,
|
|
`http-interop/`,
|
|
`hwi/`,
|
|
`incenteev/`,
|
|
`kelvinmo/`,
|
|
`knpuniversity/`,
|
|
`league/`,
|
|
`liip/`,
|
|
`myclabs/`,
|
|
`nelmio/`,
|
|
`nesbot/`,
|
|
`oneup/`,
|
|
`php$`,
|
|
`php-amqplib/`,
|
|
`php-http/`,
|
|
`php-parallel-lint/`,
|
|
`phpdocumentor/`,
|
|
`phpro/`,
|
|
`phpseclib/`,
|
|
`phpstan/`,
|
|
`psr/`,
|
|
`pyrech/`,
|
|
`ramsey/`,
|
|
`rector/`,
|
|
`sensio/`,
|
|
`sensiolabs/`,
|
|
`snc/`,
|
|
`spatie/`,
|
|
`stof/`,
|
|
`symfony/`,
|
|
`tgalopin/`,
|
|
`thecodingmachine/`,
|
|
`twig/`,
|
|
`vich/`,
|
|
`wearejust/`,
|
|
`white-october/`,
|
|
`wikimedia/`,
|
|
`yokai/`,
|
|
}
|
|
excluded, err := regexp.Compile(`^(` + strings.Join(parts, `|`) + `)`)
|
|
// Regex is a literal, so it should never happen.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for id, ver := range reqs {
|
|
if excluded.MatchString(string(id)) {
|
|
continue
|
|
}
|
|
n.Links[id] = ver
|
|
}
|
|
}
|
|
|
|
func scanComposer(dir string, nodes map[string]node, path string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dec := json.NewDecoder(f)
|
|
c := composer{}
|
|
if err := dec.Decode(&c); err != nil {
|
|
return err
|
|
}
|
|
shortPath := path[len(dir)+1 : len(path)-len(ComposerName)-1]
|
|
if c.ID == "" {
|
|
c.ID = projectID(strings.Join([]string{"__magic", shortPath}, "/"))
|
|
}
|
|
n := node{
|
|
ID: c.ID,
|
|
Path: shortPath,
|
|
Links: map[projectID]semverClause{},
|
|
}
|
|
n.SetRequire(c.Require)
|
|
nodes[shortPath] = n
|
|
return nil
|
|
}
|
|
|
|
func walk(nodes map[string]node, dir string) error {
|
|
return filepath.WalkDir(dir, func() fs.WalkDirFunc {
|
|
return func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := d.Name()
|
|
if name != ComposerName {
|
|
return nil
|
|
}
|
|
return scanComposer(dir, nodes, path)
|
|
}
|
|
}())
|
|
}
|
|
|
|
func main() {
|
|
nodes := make(map[string]node)
|
|
if len(os.Args) != 2 {
|
|
log.Fatalf("Usage: %s <root dir>", os.Args[0])
|
|
}
|
|
dirName := os.Args[1]
|
|
stat, err := os.Stat(dirName)
|
|
if err != nil {
|
|
log.Fatalf("Cannot read %s: %v", dirName, err)
|
|
}
|
|
if !stat.IsDir() {
|
|
log.Fatalf("%s is not a directory", dirName)
|
|
}
|
|
err = walk(nodes, dirName)
|
|
if err != nil {
|
|
log.Fatalf("Walking %s: %v", dirName, err)
|
|
}
|
|
tpl, err := template.New("dot").Parse(tpl)
|
|
if err != nil {
|
|
log.Fatalf("Parsing template: %v", err)
|
|
}
|
|
_ = tpl.Execute(os.Stdout, nodes)
|
|
}
|