| // Command apidiff determines whether two versions of a package are compatible |
| package main |
| |
| import ( |
| "bufio" |
| "flag" |
| "fmt" |
| "go/token" |
| "go/types" |
| "os" |
| "strings" |
| |
| "golang.org/x/exp/apidiff" |
| "golang.org/x/tools/go/gcexportdata" |
| "golang.org/x/tools/go/packages" |
| ) |
| |
| var ( |
| exportDataOutfile = flag.String("w", "", "file for export data") |
| incompatibleOnly = flag.Bool("incompatible", false, "display only incompatible changes") |
| allowInternal = flag.Bool("allow-internal", false, "allow apidiff to compare internal packages") |
| moduleMode = flag.Bool("m", false, "compare modules instead of packages") |
| ) |
| |
| func main() { |
| flag.Usage = func() { |
| w := flag.CommandLine.Output() |
| fmt.Fprintf(w, "usage:\n") |
| fmt.Fprintf(w, "apidiff OLD NEW\n") |
| fmt.Fprintf(w, " compares OLD and NEW package APIs\n") |
| fmt.Fprintf(w, " where OLD and NEW are either import paths or files of export data\n") |
| fmt.Fprintf(w, "apidiff -m OLD NEW\n") |
| fmt.Fprintf(w, " compares OLD and NEW module APIs\n") |
| fmt.Fprintf(w, " where OLD and NEW are module paths\n") |
| fmt.Fprintf(w, "apidiff -w FILE IMPORT_PATH\n") |
| fmt.Fprintf(w, " writes export data of the package at IMPORT_PATH to FILE\n") |
| fmt.Fprintf(w, " NOTE: In a GOPATH-less environment, this option consults the\n") |
| fmt.Fprintf(w, " module cache by default, unless used in the directory that\n") |
| fmt.Fprintf(w, " contains the go.mod module definition that IMPORT_PATH belongs\n") |
| fmt.Fprintf(w, " to. In most cases users want the latter behavior, so be sure\n") |
| fmt.Fprintf(w, " to cd to the exact directory which contains the module\n") |
| fmt.Fprintf(w, " definition of IMPORT_PATH.\n") |
| fmt.Fprintf(w, "apidiff -m -w FILE MODULE_PATH\n") |
| fmt.Fprintf(w, " writes export data of the module at MODULE_PATH to FILE\n") |
| fmt.Fprintf(w, " Same NOTE for packages applies to modules.\n") |
| flag.PrintDefaults() |
| } |
| |
| flag.Parse() |
| if *exportDataOutfile != "" { |
| if len(flag.Args()) != 1 { |
| flag.Usage() |
| os.Exit(2) |
| } |
| if err := loadAndWrite(flag.Arg(0)); err != nil { |
| die("writing export data: %v", err) |
| } |
| os.Exit(0) |
| } |
| |
| if len(flag.Args()) != 2 { |
| flag.Usage() |
| os.Exit(2) |
| } |
| |
| var report apidiff.Report |
| if *moduleMode { |
| oldmod := mustLoadOrReadModule(flag.Arg(0)) |
| newmod := mustLoadOrReadModule(flag.Arg(1)) |
| |
| report = apidiff.ModuleChanges(oldmod, newmod) |
| } else { |
| oldpkg := mustLoadOrReadPackage(flag.Arg(0)) |
| newpkg := mustLoadOrReadPackage(flag.Arg(1)) |
| if !*allowInternal { |
| if isInternalPackage(oldpkg.Path(), "") && isInternalPackage(newpkg.Path(), "") { |
| fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", oldpkg.Path()) |
| os.Exit(0) |
| } |
| } |
| report = apidiff.Changes(oldpkg, newpkg) |
| } |
| |
| var err error |
| if *incompatibleOnly { |
| err = report.TextIncompatible(os.Stdout, false) |
| } else { |
| err = report.Text(os.Stdout) |
| } |
| if err != nil { |
| die("writing report: %v", err) |
| } |
| } |
| |
| func loadAndWrite(path string) error { |
| if *moduleMode { |
| module := mustLoadModule(path) |
| return writeModuleExportData(module, *exportDataOutfile) |
| } |
| |
| // Loading and writing data for only a single package. |
| pkg := mustLoadPackage(path) |
| return writePackageExportData(pkg, *exportDataOutfile) |
| } |
| |
| func mustLoadOrReadPackage(importPathOrFile string) *types.Package { |
| fileInfo, err := os.Stat(importPathOrFile) |
| if err == nil && fileInfo.Mode().IsRegular() { |
| pkg, err := readPackageExportData(importPathOrFile) |
| if err != nil { |
| die("reading export data from %s: %v", importPathOrFile, err) |
| } |
| return pkg |
| } else { |
| return mustLoadPackage(importPathOrFile).Types |
| } |
| } |
| |
| func mustLoadPackage(importPath string) *packages.Package { |
| pkg, err := loadPackage(importPath) |
| if err != nil { |
| die("loading %s: %v", importPath, err) |
| } |
| return pkg |
| } |
| |
| func loadPackage(importPath string) (*packages.Package, error) { |
| cfg := &packages.Config{Mode: packages.LoadTypes | |
| packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps, |
| } |
| pkgs, err := packages.Load(cfg, importPath) |
| if err != nil { |
| return nil, err |
| } |
| if len(pkgs) == 0 { |
| return nil, fmt.Errorf("found no packages for import %s", importPath) |
| } |
| if len(pkgs[0].Errors) > 0 { |
| // TODO: use errors.Join once Go 1.21 is released. |
| return nil, pkgs[0].Errors[0] |
| } |
| return pkgs[0], nil |
| } |
| |
| func mustLoadOrReadModule(modulePathOrFile string) *apidiff.Module { |
| var module *apidiff.Module |
| fileInfo, err := os.Stat(modulePathOrFile) |
| if err == nil && fileInfo.Mode().IsRegular() { |
| module, err = readModuleExportData(modulePathOrFile) |
| if err != nil { |
| die("reading export data from %s: %v", modulePathOrFile, err) |
| } |
| } else { |
| module = mustLoadModule(modulePathOrFile) |
| } |
| |
| filterInternal(module, *allowInternal) |
| |
| return module |
| } |
| |
| func mustLoadModule(modulepath string) *apidiff.Module { |
| module, err := loadModule(modulepath) |
| if err != nil { |
| die("loading %s: %v", modulepath, err) |
| } |
| return module |
| } |
| |
| func loadModule(modulepath string) (*apidiff.Module, error) { |
| cfg := &packages.Config{Mode: packages.LoadTypes | |
| packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps | packages.NeedModule, |
| } |
| loaded, err := packages.Load(cfg, fmt.Sprintf("%s/...", modulepath)) |
| if err != nil { |
| return nil, err |
| } |
| if len(loaded) == 0 { |
| return nil, fmt.Errorf("found no packages for module %s", modulepath) |
| } |
| var tpkgs []*types.Package |
| for _, p := range loaded { |
| if len(p.Errors) > 0 { |
| // TODO: use errors.Join once Go 1.21 is released. |
| return nil, p.Errors[0] |
| } |
| tpkgs = append(tpkgs, p.Types) |
| } |
| |
| return &apidiff.Module{Path: loaded[0].Module.Path, Packages: tpkgs}, nil |
| } |
| |
| func readModuleExportData(filename string) (*apidiff.Module, error) { |
| f, err := os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| r := bufio.NewReader(f) |
| modPath, err := r.ReadString('\n') |
| if err != nil { |
| return nil, err |
| } |
| modPath = modPath[:len(modPath)-1] // remove delimiter |
| m := map[string]*types.Package{} |
| pkgs, err := gcexportdata.ReadBundle(r, token.NewFileSet(), m) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &apidiff.Module{Path: modPath, Packages: pkgs}, nil |
| } |
| |
| func writeModuleExportData(module *apidiff.Module, filename string) error { |
| f, err := os.Create(filename) |
| if err != nil { |
| return err |
| } |
| fmt.Fprintln(f, module.Path) |
| // TODO: Determine if token.NewFileSet is appropriate here. |
| if err := gcexportdata.WriteBundle(f, token.NewFileSet(), module.Packages); err != nil { |
| return err |
| } |
| return f.Close() |
| } |
| |
| func readPackageExportData(filename string) (*types.Package, error) { |
| f, err := os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| r := bufio.NewReader(f) |
| m := map[string]*types.Package{} |
| pkgPath, err := r.ReadString('\n') |
| if err != nil { |
| return nil, err |
| } |
| pkgPath = pkgPath[:len(pkgPath)-1] // remove delimiter |
| return gcexportdata.Read(r, token.NewFileSet(), m, pkgPath) |
| } |
| |
| func writePackageExportData(pkg *packages.Package, filename string) error { |
| f, err := os.Create(filename) |
| if err != nil { |
| return err |
| } |
| // Include the package path in the file. The exportdata format does |
| // not record the path of the package being written. |
| fmt.Fprintln(f, pkg.PkgPath) |
| err1 := gcexportdata.Write(f, pkg.Fset, pkg.Types) |
| err2 := f.Close() |
| if err1 != nil { |
| return err1 |
| } |
| return err2 |
| } |
| |
| func die(format string, args ...interface{}) { |
| fmt.Fprintf(os.Stderr, format+"\n", args...) |
| os.Exit(1) |
| } |
| |
| func filterInternal(m *apidiff.Module, allow bool) { |
| if allow { |
| return |
| } |
| |
| var nonInternal []*types.Package |
| for _, p := range m.Packages { |
| if !isInternalPackage(p.Path(), m.Path) { |
| nonInternal = append(nonInternal, p) |
| } else { |
| fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", p.Path()) |
| } |
| } |
| m.Packages = nonInternal |
| } |
| |
| func isInternalPackage(pkgPath, modulePath string) bool { |
| pkgPath = strings.TrimPrefix(pkgPath, modulePath) |
| switch { |
| case strings.HasSuffix(pkgPath, "/internal"): |
| return true |
| case strings.Contains(pkgPath, "/internal/"): |
| return true |
| case pkgPath == "internal": |
| return true |
| case strings.HasPrefix(pkgPath, "internal/"): |
| return true |
| } |
| return false |
| } |