golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/apidiff/main.go (about) 1 // Command apidiff determines whether two versions of a package are compatible 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 // Loading and writing data for only a single package. 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 // TODO: use errors.Join once Go 1.21 is released. 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 // TODO: use errors.Join once Go 1.21 is released. 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] // remove delimiter 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 // TODO: Determine if token.NewFileSet is appropriate here. 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] // remove delimiter 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 // Include the package path in the file. The exportdata format does 249 // not record the path of the package being written. 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 }