469 lines
13 KiB
Go
469 lines
13 KiB
Go
// 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] <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 -package option is given, bundle uses that name.
|
|
// Otherwise, if the -dst option is given, bundle uses the last
|
|
// element of the destination import path.
|
|
// Otherwise, by default bundle uses the package name found in the
|
|
// package sources in the current directory.
|
|
//
|
|
// 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 rewriting the import "golang.org/x/net/http2/hpack"
|
|
// to "internal/golang.org/x/net/http2/hpack":
|
|
//
|
|
// cd $GOROOT/src/net/http
|
|
// bundle -o h2_bundle.go \
|
|
// -prefix http2 \
|
|
// -import golang.org/x/net/http2/hpack=internal/golang.org/x/net/http2/hpack \
|
|
// golang.org/x/net/http2
|
|
//
|
|
// Two ways to update the http2 bundle:
|
|
//
|
|
// go generate net/http
|
|
//
|
|
// cd $GOROOT/src/net/http
|
|
// go generate
|
|
//
|
|
// Update both bundles, restricting ``go generate'' to running bundle commands:
|
|
//
|
|
// go generate -run bundle cmd/dist net/http
|
|
//
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"go/types"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/loader"
|
|
)
|
|
|
|
var (
|
|
outputFile = flag.String("o", "", "write output to `file` (default standard output)")
|
|
dstPath = flag.String("dst", "", "set destination import `path` (default taken from current directory)")
|
|
pkgName = flag.String("pkg", "", "set destination package `name` (default taken from current directory)")
|
|
prefix = flag.String("prefix", "", "set bundled identifier prefix to `p` (default source package name + \"_\")")
|
|
underscore = flag.Bool("underscore", false, "rewrite golang.org to golang_org in imports; temporary workaround for golang.org/issue/16333")
|
|
|
|
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)
|
|
}
|
|
|
|
if *dstPath != "" {
|
|
if *pkgName == "" {
|
|
*pkgName = path.Base(*dstPath)
|
|
}
|
|
} else {
|
|
wd, _ := os.Getwd()
|
|
pkg, err := build.ImportDir(wd, 0)
|
|
if err != nil {
|
|
log.Fatalf("cannot find package in current directory: %v", err)
|
|
}
|
|
*dstPath = pkg.ImportPath
|
|
if *pkgName == "" {
|
|
*pkgName = pkg.Name
|
|
}
|
|
}
|
|
|
|
code, err := bundle(args[0], *dstPath, *pkgName, *prefix)
|
|
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 ctxt = &build.Default
|
|
|
|
func bundle(src, dst, dstpkg, prefix string) ([]byte, error) {
|
|
// Load the initial package.
|
|
conf := loader.Config{ParserMode: parser.ParseComments, Build: ctxt}
|
|
conf.TypeCheckFuncBodies = func(p string) bool { return p == src }
|
|
conf.Import(src)
|
|
|
|
lprog, err := conf.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Because there was a single Import call and Load succeeded,
|
|
// InitialPackages is guaranteed to hold the sole requested package.
|
|
info := lprog.InitialPackages()[0]
|
|
if prefix == "" {
|
|
pkgName := info.Files[0].Name.Name
|
|
prefix = pkgName + "_"
|
|
}
|
|
|
|
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 info.Uses {
|
|
if obj == from {
|
|
if field := info.Defs[id]; field != nil {
|
|
rename(field)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rename each package-level object.
|
|
scope := info.Pkg.Scope()
|
|
for _, name := range scope.Names() {
|
|
rename(scope.Lookup(name))
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
|
|
fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n")
|
|
if *outputFile != "" {
|
|
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 info.Files {
|
|
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 info.Files {
|
|
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 conf.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 {
|
|
if *underscore {
|
|
spec = strings.Replace(spec, "golang.org/", "golang_org/", 1)
|
|
}
|
|
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 info.Files {
|
|
// Update renamed identifiers.
|
|
for id, obj := range info.Defs {
|
|
if objsToUpdate[obj] {
|
|
id.Name = prefix + obj.Name()
|
|
}
|
|
}
|
|
for id, obj := range info.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 := info.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, lprog.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, lprog.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 "" }
|