| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Bundle creates a single-source-file version of a source package |
| // suitable for inclusion in a particular target package. |
| // |
| // Usage: |
| // |
| // bundle [-o file] [-dst path] [-pkg name] [-prefix p] [-import old=new] [-tags build_constraints] <src> |
| // |
| // The src argument specifies the import path of the package to bundle. |
| // The bundling of a directory of source files into a single source file |
| // necessarily imposes a number of constraints. |
| // The package being bundled must not use cgo; must not use conditional |
| // file compilation, whether with build tags or system-specific file names |
| // like code_amd64.go; must not depend on any special comments, which |
| // may not be preserved; must not use any assembly sources; |
| // must not use renaming imports; and must not use reflection-based APIs |
| // that depend on the specific names of types or struct fields. |
| // |
| // By default, bundle writes the bundled code to standard output. |
| // If the -o argument is given, bundle writes to the named file |
| // and also includes a ``//go:generate'' comment giving the exact |
| // command line used, for regenerating the file with ``go generate.'' |
| // |
| // Bundle customizes its output for inclusion in a particular package, the destination package. |
| // By default bundle assumes the destination is the package in the current directory, |
| // but the destination package can be specified explicitly using the -dst option, |
| // which takes an import path as its argument. |
| // If the source package imports the destination package, bundle will remove |
| // those imports and rewrite any references to use direct references to the |
| // corresponding symbols. |
| // Bundle also must write a package declaration in the output and must |
| // choose a name to use in that declaration. |
| // If the -pkg option is given, bundle uses that name. |
| // Otherwise, the name of the destination package is used. |
| // Build constraints for the generated file can be specified using the -tags option. |
| // |
| // To avoid collisions, bundle inserts a prefix at the beginning of |
| // every package-level const, func, type, and var identifier in src's code, |
| // updating references accordingly. The default prefix is the package name |
| // of the source package followed by an underscore. The -prefix option |
| // specifies an alternate prefix. |
| // |
| // Occasionally it is necessary to rewrite imports during the bundling |
| // process. The -import option, which may be repeated, specifies that |
| // an import of "old" should be rewritten to import "new" instead. |
| // |
| // Example |
| // |
| // Bundle archive/zip for inclusion in cmd/dist: |
| // |
| // cd $GOROOT/src/cmd/dist |
| // bundle -o zip.go archive/zip |
| // |
| // Bundle golang.org/x/net/http2 for inclusion in net/http, |
| // prefixing all identifiers by "http2" instead of "http2_", and |
| // including a "!nethttpomithttp2" build constraint: |
| // |
| // cd $GOROOT/src/net/http |
| // bundle -o h2_bundle.go -prefix http2 -tags '!nethttpomithttp2' golang.org/x/net/http2 |
| // |
| // Update the http2 bundle in net/http: |
| // |
| // go generate net/http |
| // |
| // Update all bundles in the standard library: |
| // |
| // go generate -run bundle std |
| // |
| package main |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/format" |
| "go/printer" |
| "go/token" |
| "go/types" |
| "io/ioutil" |
| "log" |
| "os" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/tools/go/packages" |
| ) |
| |
| var ( |
| outputFile = flag.String("o", "", "write output to `file` (default standard output)") |
| dstPath = flag.String("dst", ".", "set destination import `path`") |
| pkgName = flag.String("pkg", "", "set destination package `name`") |
| prefix = flag.String("prefix", "&_", "set bundled identifier prefix to `p` (default is \"&_\", where & stands for the original name)") |
| buildTags = flag.String("tags", "", "the build constraints to be inserted into the generated file") |
| |
| importMap = map[string]string{} |
| ) |
| |
| func init() { |
| flag.Var(flagFunc(addImportMap), "import", "rewrite import using `map`, of form old=new (can be repeated)") |
| } |
| |
| func addImportMap(s string) { |
| if strings.Count(s, "=") != 1 { |
| log.Fatal("-import argument must be of the form old=new") |
| } |
| i := strings.Index(s, "=") |
| old, new := s[:i], s[i+1:] |
| if old == "" || new == "" { |
| log.Fatal("-import argument must be of the form old=new; old and new must be non-empty") |
| } |
| importMap[old] = new |
| } |
| |
| func usage() { |
| fmt.Fprintf(os.Stderr, "Usage: bundle [options] <src>\n") |
| flag.PrintDefaults() |
| } |
| |
| func main() { |
| log.SetPrefix("bundle: ") |
| log.SetFlags(0) |
| |
| flag.Usage = usage |
| flag.Parse() |
| args := flag.Args() |
| if len(args) != 1 { |
| usage() |
| os.Exit(2) |
| } |
| |
| cfg := &packages.Config{Mode: packages.NeedName} |
| pkgs, err := packages.Load(cfg, *dstPath) |
| if err != nil { |
| log.Fatalf("cannot load destination package: %v", err) |
| } |
| if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 { |
| log.Fatalf("failed to load destination package") |
| } |
| if *pkgName == "" { |
| *pkgName = pkgs[0].Name |
| } |
| |
| code, err := bundle(args[0], pkgs[0].PkgPath, *pkgName, *prefix, *buildTags) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if *outputFile != "" { |
| err := ioutil.WriteFile(*outputFile, code, 0666) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } else { |
| _, err := os.Stdout.Write(code) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } |
| } |
| |
| // isStandardImportPath is copied from cmd/go in the standard library. |
| func isStandardImportPath(path string) bool { |
| i := strings.Index(path, "/") |
| if i < 0 { |
| i = len(path) |
| } |
| elem := path[:i] |
| return !strings.Contains(elem, ".") |
| } |
| |
| var testingOnlyPackagesConfig *packages.Config |
| |
| func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) { |
| // Load the initial package. |
| cfg := &packages.Config{} |
| if testingOnlyPackagesConfig != nil { |
| *cfg = *testingOnlyPackagesConfig |
| } else { |
| // Bypass default vendor mode, as we need a package not available in the |
| // std module vendor folder. |
| cfg.Env = append(os.Environ(), "GOFLAGS=-mod=mod") |
| } |
| cfg.Mode = packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo |
| pkgs, err := packages.Load(cfg, src) |
| if err != nil { |
| return nil, err |
| } |
| if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 { |
| return nil, fmt.Errorf("failed to load source package") |
| } |
| pkg := pkgs[0] |
| |
| if strings.Contains(prefix, "&") { |
| prefix = strings.Replace(prefix, "&", pkg.Syntax[0].Name.Name, -1) |
| } |
| |
| objsToUpdate := make(map[types.Object]bool) |
| var rename func(from types.Object) |
| rename = func(from types.Object) { |
| if !objsToUpdate[from] { |
| objsToUpdate[from] = true |
| |
| // Renaming a type that is used as an embedded field |
| // requires renaming the field too. e.g. |
| // type T int // if we rename this to U.. |
| // var s struct {T} |
| // print(s.T) // ...this must change too |
| if _, ok := from.(*types.TypeName); ok { |
| for id, obj := range pkg.TypesInfo.Uses { |
| if obj == from { |
| if field := pkg.TypesInfo.Defs[id]; field != nil { |
| rename(field) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Rename each package-level object. |
| scope := pkg.Types.Scope() |
| for _, name := range scope.Names() { |
| rename(scope.Lookup(name)) |
| } |
| |
| var out bytes.Buffer |
| if buildTags != "" { |
| fmt.Fprintf(&out, "// +build %s\n\n", buildTags) |
| } |
| |
| fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n") |
| if *outputFile != "" && buildTags == "" { |
| fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(os.Args[1:], " ")) |
| } else { |
| fmt.Fprintf(&out, "// $ bundle %s\n", strings.Join(os.Args[1:], " ")) |
| } |
| fmt.Fprintf(&out, "\n") |
| |
| // Concatenate package comments from all files... |
| for _, f := range pkg.Syntax { |
| if doc := f.Doc.Text(); strings.TrimSpace(doc) != "" { |
| for _, line := range strings.Split(doc, "\n") { |
| fmt.Fprintf(&out, "// %s\n", line) |
| } |
| } |
| } |
| // ...but don't let them become the actual package comment. |
| fmt.Fprintln(&out) |
| |
| fmt.Fprintf(&out, "package %s\n\n", dstpkg) |
| |
| // BUG(adonovan,shurcooL): bundle may generate incorrect code |
| // due to shadowing between identifiers and imported package names. |
| // |
| // The generated code will either fail to compile or |
| // (unlikely) compile successfully but have different behavior |
| // than the original package. The risk of this happening is higher |
| // when the original package has renamed imports (they're typically |
| // renamed in order to resolve a shadow inside that particular .go file). |
| |
| // TODO(adonovan,shurcooL): |
| // - detect shadowing issues, and either return error or resolve them |
| // - preserve comments from the original import declarations. |
| |
| // pkgStd and pkgExt are sets of printed import specs. This is done |
| // to deduplicate instances of the same import name and path. |
| var pkgStd = make(map[string]bool) |
| var pkgExt = make(map[string]bool) |
| for _, f := range pkg.Syntax { |
| for _, imp := range f.Imports { |
| path, err := strconv.Unquote(imp.Path.Value) |
| if err != nil { |
| log.Fatalf("invalid import path string: %v", err) // Shouldn't happen here since packages.Load succeeded. |
| } |
| if path == dst { |
| continue |
| } |
| if newPath, ok := importMap[path]; ok { |
| path = newPath |
| } |
| |
| var name string |
| if imp.Name != nil { |
| name = imp.Name.Name |
| } |
| spec := fmt.Sprintf("%s %q", name, path) |
| if isStandardImportPath(path) { |
| pkgStd[spec] = true |
| } else { |
| pkgExt[spec] = true |
| } |
| } |
| } |
| |
| // Print a single declaration that imports all necessary packages. |
| fmt.Fprintln(&out, "import (") |
| for p := range pkgStd { |
| fmt.Fprintf(&out, "\t%s\n", p) |
| } |
| if len(pkgExt) > 0 { |
| fmt.Fprintln(&out) |
| } |
| for p := range pkgExt { |
| fmt.Fprintf(&out, "\t%s\n", p) |
| } |
| fmt.Fprint(&out, ")\n\n") |
| |
| // Modify and print each file. |
| for _, f := range pkg.Syntax { |
| // Update renamed identifiers. |
| for id, obj := range pkg.TypesInfo.Defs { |
| if objsToUpdate[obj] { |
| id.Name = prefix + obj.Name() |
| } |
| } |
| for id, obj := range pkg.TypesInfo.Uses { |
| if objsToUpdate[obj] { |
| id.Name = prefix + obj.Name() |
| } |
| } |
| |
| // For each qualified identifier that refers to the |
| // destination package, remove the qualifier. |
| // The "@@@." strings are removed in postprocessing. |
| ast.Inspect(f, func(n ast.Node) bool { |
| if sel, ok := n.(*ast.SelectorExpr); ok { |
| if id, ok := sel.X.(*ast.Ident); ok { |
| if obj, ok := pkg.TypesInfo.Uses[id].(*types.PkgName); ok { |
| if obj.Imported().Path() == dst { |
| id.Name = "@@@" |
| } |
| } |
| } |
| } |
| return true |
| }) |
| |
| last := f.Package |
| if len(f.Imports) > 0 { |
| imp := f.Imports[len(f.Imports)-1] |
| last = imp.End() |
| if imp.Comment != nil { |
| if e := imp.Comment.End(); e > last { |
| last = e |
| } |
| } |
| } |
| |
| // Pretty-print package-level declarations. |
| // but no package or import declarations. |
| var buf bytes.Buffer |
| for _, decl := range f.Decls { |
| if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT { |
| continue |
| } |
| |
| beg, end := sourceRange(decl) |
| |
| printComments(&out, f.Comments, last, beg) |
| |
| buf.Reset() |
| format.Node(&buf, pkg.Fset, &printer.CommentedNode{Node: decl, Comments: f.Comments}) |
| // Remove each "@@@." in the output. |
| // TODO(adonovan): not hygienic. |
| out.Write(bytes.Replace(buf.Bytes(), []byte("@@@."), nil, -1)) |
| |
| last = printSameLineComment(&out, f.Comments, pkg.Fset, end) |
| |
| out.WriteString("\n\n") |
| } |
| |
| printLastComments(&out, f.Comments, last) |
| } |
| |
| // Now format the entire thing. |
| result, err := format.Source(out.Bytes()) |
| if err != nil { |
| log.Fatalf("formatting failed: %v", err) |
| } |
| |
| return result, nil |
| } |
| |
| // sourceRange returns the [beg, end) interval of source code |
| // belonging to decl (incl. associated comments). |
| func sourceRange(decl ast.Decl) (beg, end token.Pos) { |
| beg = decl.Pos() |
| end = decl.End() |
| |
| var doc, com *ast.CommentGroup |
| |
| switch d := decl.(type) { |
| case *ast.GenDecl: |
| doc = d.Doc |
| if len(d.Specs) > 0 { |
| switch spec := d.Specs[len(d.Specs)-1].(type) { |
| case *ast.ValueSpec: |
| com = spec.Comment |
| case *ast.TypeSpec: |
| com = spec.Comment |
| } |
| } |
| case *ast.FuncDecl: |
| doc = d.Doc |
| } |
| |
| if doc != nil { |
| beg = doc.Pos() |
| } |
| if com != nil && com.End() > end { |
| end = com.End() |
| } |
| |
| return beg, end |
| } |
| |
| func printComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos, end token.Pos) { |
| for _, cg := range comments { |
| if pos <= cg.Pos() && cg.Pos() < end { |
| for _, c := range cg.List { |
| fmt.Fprintln(out, c.Text) |
| } |
| fmt.Fprintln(out) |
| } |
| } |
| } |
| |
| const infinity = 1 << 30 |
| |
| func printLastComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos token.Pos) { |
| printComments(out, comments, pos, infinity) |
| } |
| |
| func printSameLineComment(out *bytes.Buffer, comments []*ast.CommentGroup, fset *token.FileSet, pos token.Pos) token.Pos { |
| tf := fset.File(pos) |
| for _, cg := range comments { |
| if pos <= cg.Pos() && tf.Line(cg.Pos()) == tf.Line(pos) { |
| for _, c := range cg.List { |
| fmt.Fprintln(out, c.Text) |
| } |
| return cg.End() |
| } |
| } |
| return pos |
| } |
| |
| type flagFunc func(string) |
| |
| func (f flagFunc) Set(s string) error { |
| f(s) |
| return nil |
| } |
| |
| func (f flagFunc) String() string { return "" } |