1
2 package main
3
4 import (
5 "bufio"
6 "flag"
7 "fmt"
8 "go/token"
9 "go/types"
10 "os"
11 "strings"
12
13 "golang.org/x/exp/apidiff"
14 "golang.org/x/tools/go/gcexportdata"
15 "golang.org/x/tools/go/packages"
16 )
17
18 var (
19 exportDataOutfile = flag.String("w", "", "file for export data")
20 incompatibleOnly = flag.Bool("incompatible", false, "display only incompatible changes")
21 allowInternal = flag.Bool("allow-internal", false, "allow apidiff to compare internal packages")
22 moduleMode = flag.Bool("m", false, "compare modules instead of packages")
23 )
24
25 func main() {
26 flag.Usage = func() {
27 w := flag.CommandLine.Output()
28 fmt.Fprintf(w, "usage:\n")
29 fmt.Fprintf(w, "apidiff OLD NEW\n")
30 fmt.Fprintf(w, " compares OLD and NEW package APIs\n")
31 fmt.Fprintf(w, " where OLD and NEW are either import paths or files of export data\n")
32 fmt.Fprintf(w, "apidiff -m OLD NEW\n")
33 fmt.Fprintf(w, " compares OLD and NEW module APIs\n")
34 fmt.Fprintf(w, " where OLD and NEW are module paths\n")
35 fmt.Fprintf(w, "apidiff -w FILE IMPORT_PATH\n")
36 fmt.Fprintf(w, " writes export data of the package at IMPORT_PATH to FILE\n")
37 fmt.Fprintf(w, " NOTE: In a GOPATH-less environment, this option consults the\n")
38 fmt.Fprintf(w, " module cache by default, unless used in the directory that\n")
39 fmt.Fprintf(w, " contains the go.mod module definition that IMPORT_PATH belongs\n")
40 fmt.Fprintf(w, " to. In most cases users want the latter behavior, so be sure\n")
41 fmt.Fprintf(w, " to cd to the exact directory which contains the module\n")
42 fmt.Fprintf(w, " definition of IMPORT_PATH.\n")
43 fmt.Fprintf(w, "apidiff -m -w FILE MODULE_PATH\n")
44 fmt.Fprintf(w, " writes export data of the module at MODULE_PATH to FILE\n")
45 fmt.Fprintf(w, " Same NOTE for packages applies to modules.\n")
46 flag.PrintDefaults()
47 }
48
49 flag.Parse()
50 if *exportDataOutfile != "" {
51 if len(flag.Args()) != 1 {
52 flag.Usage()
53 os.Exit(2)
54 }
55 if err := loadAndWrite(flag.Arg(0)); err != nil {
56 die("writing export data: %v", err)
57 }
58 os.Exit(0)
59 }
60
61 if len(flag.Args()) != 2 {
62 flag.Usage()
63 os.Exit(2)
64 }
65
66 var report apidiff.Report
67 if *moduleMode {
68 oldmod := mustLoadOrReadModule(flag.Arg(0))
69 newmod := mustLoadOrReadModule(flag.Arg(1))
70
71 report = apidiff.ModuleChanges(oldmod, newmod)
72 } else {
73 oldpkg := mustLoadOrReadPackage(flag.Arg(0))
74 newpkg := mustLoadOrReadPackage(flag.Arg(1))
75 if !*allowInternal {
76 if isInternalPackage(oldpkg.Path(), "") && isInternalPackage(newpkg.Path(), "") {
77 fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", oldpkg.Path())
78 os.Exit(0)
79 }
80 }
81 report = apidiff.Changes(oldpkg, newpkg)
82 }
83
84 var err error
85 if *incompatibleOnly {
86 err = report.TextIncompatible(os.Stdout, false)
87 } else {
88 err = report.Text(os.Stdout)
89 }
90 if err != nil {
91 die("writing report: %v", err)
92 }
93 }
94
95 func loadAndWrite(path string) error {
96 if *moduleMode {
97 module := mustLoadModule(path)
98 return writeModuleExportData(module, *exportDataOutfile)
99 }
100
101
102 pkg := mustLoadPackage(path)
103 return writePackageExportData(pkg, *exportDataOutfile)
104 }
105
106 func mustLoadOrReadPackage(importPathOrFile string) *types.Package {
107 fileInfo, err := os.Stat(importPathOrFile)
108 if err == nil && fileInfo.Mode().IsRegular() {
109 pkg, err := readPackageExportData(importPathOrFile)
110 if err != nil {
111 die("reading export data from %s: %v", importPathOrFile, err)
112 }
113 return pkg
114 } else {
115 return mustLoadPackage(importPathOrFile).Types
116 }
117 }
118
119 func mustLoadPackage(importPath string) *packages.Package {
120 pkg, err := loadPackage(importPath)
121 if err != nil {
122 die("loading %s: %v", importPath, err)
123 }
124 return pkg
125 }
126
127 func loadPackage(importPath string) (*packages.Package, error) {
128 cfg := &packages.Config{Mode: packages.LoadTypes |
129 packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
130 }
131 pkgs, err := packages.Load(cfg, importPath)
132 if err != nil {
133 return nil, err
134 }
135 if len(pkgs) == 0 {
136 return nil, fmt.Errorf("found no packages for import %s", importPath)
137 }
138 if len(pkgs[0].Errors) > 0 {
139
140 return nil, pkgs[0].Errors[0]
141 }
142 return pkgs[0], nil
143 }
144
145 func mustLoadOrReadModule(modulePathOrFile string) *apidiff.Module {
146 var module *apidiff.Module
147 fileInfo, err := os.Stat(modulePathOrFile)
148 if err == nil && fileInfo.Mode().IsRegular() {
149 module, err = readModuleExportData(modulePathOrFile)
150 if err != nil {
151 die("reading export data from %s: %v", modulePathOrFile, err)
152 }
153 } else {
154 module = mustLoadModule(modulePathOrFile)
155 }
156
157 filterInternal(module, *allowInternal)
158
159 return module
160 }
161
162 func mustLoadModule(modulepath string) *apidiff.Module {
163 module, err := loadModule(modulepath)
164 if err != nil {
165 die("loading %s: %v", modulepath, err)
166 }
167 return module
168 }
169
170 func loadModule(modulepath string) (*apidiff.Module, error) {
171 cfg := &packages.Config{Mode: packages.LoadTypes |
172 packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps | packages.NeedModule,
173 }
174 loaded, err := packages.Load(cfg, fmt.Sprintf("%s/...", modulepath))
175 if err != nil {
176 return nil, err
177 }
178 if len(loaded) == 0 {
179 return nil, fmt.Errorf("found no packages for module %s", modulepath)
180 }
181 var tpkgs []*types.Package
182 for _, p := range loaded {
183 if len(p.Errors) > 0 {
184
185 return nil, p.Errors[0]
186 }
187 tpkgs = append(tpkgs, p.Types)
188 }
189
190 return &apidiff.Module{Path: loaded[0].Module.Path, Packages: tpkgs}, nil
191 }
192
193 func readModuleExportData(filename string) (*apidiff.Module, error) {
194 f, err := os.Open(filename)
195 if err != nil {
196 return nil, err
197 }
198 defer f.Close()
199 r := bufio.NewReader(f)
200 modPath, err := r.ReadString('\n')
201 if err != nil {
202 return nil, err
203 }
204 modPath = modPath[:len(modPath)-1]
205 m := map[string]*types.Package{}
206 pkgs, err := gcexportdata.ReadBundle(r, token.NewFileSet(), m)
207 if err != nil {
208 return nil, err
209 }
210
211 return &apidiff.Module{Path: modPath, Packages: pkgs}, nil
212 }
213
214 func writeModuleExportData(module *apidiff.Module, filename string) error {
215 f, err := os.Create(filename)
216 if err != nil {
217 return err
218 }
219 fmt.Fprintln(f, module.Path)
220
221 if err := gcexportdata.WriteBundle(f, token.NewFileSet(), module.Packages); err != nil {
222 return err
223 }
224 return f.Close()
225 }
226
227 func readPackageExportData(filename string) (*types.Package, error) {
228 f, err := os.Open(filename)
229 if err != nil {
230 return nil, err
231 }
232 defer f.Close()
233 r := bufio.NewReader(f)
234 m := map[string]*types.Package{}
235 pkgPath, err := r.ReadString('\n')
236 if err != nil {
237 return nil, err
238 }
239 pkgPath = pkgPath[:len(pkgPath)-1]
240 return gcexportdata.Read(r, token.NewFileSet(), m, pkgPath)
241 }
242
243 func writePackageExportData(pkg *packages.Package, filename string) error {
244 f, err := os.Create(filename)
245 if err != nil {
246 return err
247 }
248
249
250 fmt.Fprintln(f, pkg.PkgPath)
251 err1 := gcexportdata.Write(f, pkg.Fset, pkg.Types)
252 err2 := f.Close()
253 if err1 != nil {
254 return err1
255 }
256 return err2
257 }
258
259 func die(format string, args ...interface{}) {
260 fmt.Fprintf(os.Stderr, format+"\n", args...)
261 os.Exit(1)
262 }
263
264 func filterInternal(m *apidiff.Module, allow bool) {
265 if allow {
266 return
267 }
268
269 var nonInternal []*types.Package
270 for _, p := range m.Packages {
271 if !isInternalPackage(p.Path(), m.Path) {
272 nonInternal = append(nonInternal, p)
273 } else {
274 fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", p.Path())
275 }
276 }
277 m.Packages = nonInternal
278 }
279
280 func isInternalPackage(pkgPath, modulePath string) bool {
281 pkgPath = strings.TrimPrefix(pkgPath, modulePath)
282 switch {
283 case strings.HasSuffix(pkgPath, "/internal"):
284 return true
285 case strings.Contains(pkgPath, "/internal/"):
286 return true
287 case pkgPath == "internal":
288 return true
289 case strings.HasPrefix(pkgPath, "internal/"):
290 return true
291 }
292 return false
293 }
294
View as plain text