phpdeps.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package main
  2. import (
  3. _ "embed"
  4. "encoding/json"
  5. "io/fs"
  6. "log"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "strings"
  11. "text/template"
  12. )
  13. //go:embed "templates/deps.dot.tmpl"
  14. var tpl string
  15. // ComposerName is the reserved name of the Composer specifications file.
  16. const ComposerName = "composer.json"
  17. /*
  18. In most cases: <owner>/<project>, but some special cases are valid too, like
  19. "php", "ext-<extension>", "lib-<library>"
  20. */
  21. type projectID string
  22. type semverClause string
  23. type node struct {
  24. ID projectID
  25. Path string
  26. Links map[projectID]semverClause
  27. }
  28. type composer struct {
  29. ID projectID `json:"name"`
  30. Require map[projectID]semverClause
  31. RequireDev map[projectID]semverClause
  32. }
  33. func (n *node) SetRequire(reqs map[projectID]semverClause) {
  34. // todo Probably use a positive list instead of a negative list
  35. parts := []string{
  36. `api-platform/`,
  37. `aporat/`,
  38. `babdev/`,
  39. `cocur/`,
  40. `composer/`,
  41. `doctrine/`,
  42. `eo/`,
  43. `ergebnis/`,
  44. `evence/`,
  45. `ext-`,
  46. `firebase/`,
  47. `flow/`,
  48. `friendsofphp/`,
  49. `friendsofsymfony/`,
  50. `google/`,
  51. `guzzlehttp/`,
  52. `http-interop/`,
  53. `hwi/`,
  54. `incenteev/`,
  55. `kelvinmo/`,
  56. `knpuniversity/`,
  57. `league/`,
  58. `liip/`,
  59. `myclabs/`,
  60. `nelmio/`,
  61. `nesbot/`,
  62. `oneup/`,
  63. `php$`,
  64. `php-amqplib/`,
  65. `php-http/`,
  66. `php-parallel-lint/`,
  67. `phpdocumentor/`,
  68. `phpro/`,
  69. `phpseclib/`,
  70. `phpstan/`,
  71. `psr/`,
  72. `pyrech/`,
  73. `ramsey/`,
  74. `rector/`,
  75. `sensio/`,
  76. `sensiolabs/`,
  77. `snc/`,
  78. `spatie/`,
  79. `stof/`,
  80. `symfony/`,
  81. `tgalopin/`,
  82. `thecodingmachine/`,
  83. `twig/`,
  84. `vich/`,
  85. `wearejust/`,
  86. `white-october/`,
  87. `wikimedia/`,
  88. `yokai/`,
  89. }
  90. excluded, err := regexp.Compile(`^(` + strings.Join(parts, `|`) + `)`)
  91. // Regex is a literal, so it should never happen.
  92. if err != nil {
  93. panic(err)
  94. }
  95. for id, ver := range reqs {
  96. if excluded.MatchString(string(id)) {
  97. continue
  98. }
  99. n.Links[id] = ver
  100. }
  101. }
  102. func scanComposer(dir string, nodes map[string]node, path string) error {
  103. f, err := os.Open(path)
  104. if err != nil {
  105. return err
  106. }
  107. dec := json.NewDecoder(f)
  108. c := composer{}
  109. if err := dec.Decode(&c); err != nil {
  110. return err
  111. }
  112. shortPath := path[len(dir)+1 : len(path)-len(ComposerName)-1]
  113. if c.ID == "" {
  114. c.ID = projectID(strings.Join([]string{"__magic", shortPath}, "/"))
  115. }
  116. n := node{
  117. ID: c.ID,
  118. Path: shortPath,
  119. Links: map[projectID]semverClause{},
  120. }
  121. n.SetRequire(c.Require)
  122. nodes[shortPath] = n
  123. return nil
  124. }
  125. func walk(nodes map[string]node, dir string) error {
  126. return filepath.WalkDir(dir, func() fs.WalkDirFunc {
  127. return func(path string, d fs.DirEntry, err error) error {
  128. if err != nil {
  129. return err
  130. }
  131. name := d.Name()
  132. if name != ComposerName {
  133. return nil
  134. }
  135. return scanComposer(dir, nodes, path)
  136. }
  137. }())
  138. }
  139. func main() {
  140. nodes := make(map[string]node)
  141. if len(os.Args) != 2 {
  142. log.Fatalf("Usage: %s <root dir>", os.Args[0])
  143. }
  144. dirName := os.Args[1]
  145. stat, err := os.Stat(dirName)
  146. if err != nil {
  147. log.Fatalf("Cannot read %s: %v", dirName, err)
  148. }
  149. if !stat.IsDir() {
  150. log.Fatalf("%s is not a directory", dirName)
  151. }
  152. err = walk(nodes, dirName)
  153. if err != nil {
  154. log.Fatalf("Walking %s: %v", dirName, err)
  155. }
  156. tpl, err := template.New("dot").Parse(tpl)
  157. if err != nil {
  158. log.Fatalf("Parsing template: %v", err)
  159. }
  160. _ = tpl.Execute(os.Stdout, nodes)
  161. }