123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- 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)
- }
|