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: /, but some special cases are valid too, like "php", "ext-", "lib-" */ 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 ", 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) }