go_phpdeps/phpdeps.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)
}