| 1 | // Copyright 2015 The Go Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | // Bundle creates a single-source-file version of a source package |
| 6 | // suitable for inclusion in a particular target package. |
| 7 | // |
| 8 | // Usage: |
| 9 | // |
| 10 | // bundle [-o file] [-dst path] [-pkg name] [-prefix p] [-import old=new] [-tags build_constraints] <src> |
| 11 | // |
| 12 | // The src argument specifies the import path of the package to bundle. |
| 13 | // The bundling of a directory of source files into a single source file |
| 14 | // necessarily imposes a number of constraints. |
| 15 | // The package being bundled must not use cgo; must not use conditional |
| 16 | // file compilation, whether with build tags or system-specific file names |
| 17 | // like code_amd64.go; must not depend on any special comments, which |
| 18 | // may not be preserved; must not use any assembly sources; |
| 19 | // must not use renaming imports; and must not use reflection-based APIs |
| 20 | // that depend on the specific names of types or struct fields. |
| 21 | // |
| 22 | // By default, bundle writes the bundled code to standard output. |
| 23 | // If the -o argument is given, bundle writes to the named file |
| 24 | // and also includes a “//go:generate” comment giving the exact |
| 25 | // command line used, for regenerating the file with “go generate.” |
| 26 | // |
| 27 | // Bundle customizes its output for inclusion in a particular package, the destination package. |
| 28 | // By default bundle assumes the destination is the package in the current directory, |
| 29 | // but the destination package can be specified explicitly using the -dst option, |
| 30 | // which takes an import path as its argument. |
| 31 | // If the source package imports the destination package, bundle will remove |
| 32 | // those imports and rewrite any references to use direct references to the |
| 33 | // corresponding symbols. |
| 34 | // Bundle also must write a package declaration in the output and must |
| 35 | // choose a name to use in that declaration. |
| 36 | // If the -pkg option is given, bundle uses that name. |
| 37 | // Otherwise, the name of the destination package is used. |
| 38 | // Build constraints for the generated file can be specified using the -tags option. |
| 39 | // |
| 40 | // To avoid collisions, bundle inserts a prefix at the beginning of |
| 41 | // every package-level const, func, type, and var identifier in src's code, |
| 42 | // updating references accordingly. The default prefix is the package name |
| 43 | // of the source package followed by an underscore. The -prefix option |
| 44 | // specifies an alternate prefix. |
| 45 | // |
| 46 | // Occasionally it is necessary to rewrite imports during the bundling |
| 47 | // process. The -import option, which may be repeated, specifies that |
| 48 | // an import of "old" should be rewritten to import "new" instead. |
| 49 | // |
| 50 | // # Example |
| 51 | // |
| 52 | // Bundle archive/zip for inclusion in cmd/dist: |
| 53 | // |
| 54 | // cd $GOROOT/src/cmd/dist |
| 55 | // bundle -o zip.go archive/zip |
| 56 | // |
| 57 | // Bundle golang.org/x/net/http2 for inclusion in net/http, |
| 58 | // prefixing all identifiers by "http2" instead of "http2_", and |
| 59 | // including a "!nethttpomithttp2" build constraint: |
| 60 | // |
| 61 | // cd $GOROOT/src/net/http |
| 62 | // bundle -o h2_bundle.go -prefix http2 -tags '!nethttpomithttp2' golang.org/x/net/http2 |
| 63 | // |
| 64 | // Update the http2 bundle in net/http: |
| 65 | // |
| 66 | // go generate net/http |
| 67 | // |
| 68 | // Update all bundles in the standard library: |
| 69 | // |
| 70 | // go generate -run bundle std |
| 71 | package main |
| 72 | |
| 73 | import ( |
| 74 | "bytes" |
| 75 | "flag" |
| 76 | "fmt" |
| 77 | "go/ast" |
| 78 | "go/format" |
| 79 | "go/printer" |
| 80 | "go/token" |
| 81 | "go/types" |
| 82 | "io/ioutil" |
| 83 | "log" |
| 84 | "os" |
| 85 | "strconv" |
| 86 | "strings" |
| 87 | "unicode" |
| 88 | |
| 89 | "golang.org/x/tools/go/packages" |
| 90 | ) |
| 91 | |
| 92 | var ( |
| 93 | outputFile = flag.String("o", "", "write output to `file` (default standard output)") |
| 94 | dstPath = flag.String("dst", ".", "set destination import `path`") |
| 95 | pkgName = flag.String("pkg", "", "set destination package `name`") |
| 96 | prefix = flag.String("prefix", "&_", "set bundled identifier prefix to `p` (default is \"&_\", where & stands for the original name)") |
| 97 | buildTags = flag.String("tags", "", "the build constraints to be inserted into the generated file") |
| 98 | |
| 99 | importMap = map[string]string{} |
| 100 | ) |
| 101 | |
| 102 | func init() { |
| 103 | flag.Var(flagFunc(addImportMap), "import", "rewrite import using `map`, of form old=new (can be repeated)") |
| 104 | } |
| 105 | |
| 106 | func addImportMap(s string) { |
| 107 | if strings.Count(s, "=") != 1 { |
| 108 | log.Fatal("-import argument must be of the form old=new") |
| 109 | } |
| 110 | i := strings.Index(s, "=") |
| 111 | old, new := s[:i], s[i+1:] |
| 112 | if old == "" || new == "" { |
| 113 | log.Fatal("-import argument must be of the form old=new; old and new must be non-empty") |
| 114 | } |
| 115 | importMap[old] = new |
| 116 | } |
| 117 | |
| 118 | func usage() { |
| 119 | fmt.Fprintf(os.Stderr, "Usage: bundle [options] <src>\n") |
| 120 | flag.PrintDefaults() |
| 121 | } |
| 122 | |
| 123 | func main() { |
| 124 | log.SetPrefix("bundle: ") |
| 125 | log.SetFlags(0) |
| 126 | |
| 127 | flag.Usage = usage |
| 128 | flag.Parse() |
| 129 | args := flag.Args() |
| 130 | if len(args) != 1 { |
| 131 | usage() |
| 132 | os.Exit(2) |
| 133 | } |
| 134 | |
| 135 | cfg := &packages.Config{Mode: packages.NeedName} |
| 136 | pkgs, err := packages.Load(cfg, *dstPath) |
| 137 | if err != nil { |
| 138 | log.Fatalf("cannot load destination package: %v", err) |
| 139 | } |
| 140 | if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 { |
| 141 | log.Fatalf("failed to load destination package") |
| 142 | } |
| 143 | if *pkgName == "" { |
| 144 | *pkgName = pkgs[0].Name |
| 145 | } |
| 146 | |
| 147 | code, err := bundle(args[0], pkgs[0].PkgPath, *pkgName, *prefix, *buildTags) |
| 148 | if err != nil { |
| 149 | log.Fatal(err) |
| 150 | } |
| 151 | if *outputFile != "" { |
| 152 | err := ioutil.WriteFile(*outputFile, code, 0666) |
| 153 | if err != nil { |
| 154 | log.Fatal(err) |
| 155 | } |
| 156 | } else { |
| 157 | _, err := os.Stdout.Write(code) |
| 158 | if err != nil { |
| 159 | log.Fatal(err) |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | // isStandardImportPath is copied from cmd/go in the standard library. |
| 165 | func isStandardImportPath(path string) bool { |
| 166 | i := strings.Index(path, "/") |
| 167 | if i < 0 { |
| 168 | i = len(path) |
| 169 | } |
| 170 | elem := path[:i] |
| 171 | return !strings.Contains(elem, ".") |
| 172 | } |
| 173 | |
| 174 | var testingOnlyPackagesConfig *packages.Config |
| 175 | |
| 176 | func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) { |
| 177 | // Load the initial package. |
| 178 | cfg := &packages.Config{} |
| 179 | if testingOnlyPackagesConfig != nil { |
| 180 | *cfg = *testingOnlyPackagesConfig |
| 181 | } else { |
| 182 | // Bypass default vendor mode, as we need a package not available in the |
| 183 | // std module vendor folder. |
| 184 | cfg.Env = append(os.Environ(), "GOFLAGS=-mod=mod") |
| 185 | } |
| 186 | cfg.Mode = packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo |
| 187 | pkgs, err := packages.Load(cfg, src) |
| 188 | if err != nil { |
| 189 | return nil, err |
| 190 | } |
| 191 | if packages.PrintErrors(pkgs) > 0 || len(pkgs) != 1 { |
| 192 | return nil, fmt.Errorf("failed to load source package") |
| 193 | } |
| 194 | pkg := pkgs[0] |
| 195 | |
| 196 | if strings.Contains(prefix, "&") { |
| 197 | prefix = strings.Replace(prefix, "&", pkg.Syntax[0].Name.Name, -1) |
| 198 | } |
| 199 | |
| 200 | objsToUpdate := make(map[types.Object]bool) |
| 201 | var rename func(from types.Object) |
| 202 | rename = func(from types.Object) { |
| 203 | if !objsToUpdate[from] { |
| 204 | objsToUpdate[from] = true |
| 205 | |
| 206 | // Renaming a type that is used as an embedded field |
| 207 | // requires renaming the field too. e.g. |
| 208 | // type T int // if we rename this to U.. |
| 209 | // var s struct {T} |
| 210 | // print(s.T) // ...this must change too |
| 211 | if _, ok := from.(*types.TypeName); ok { |
| 212 | for id, obj := range pkg.TypesInfo.Uses { |
| 213 | if obj == from { |
| 214 | if field := pkg.TypesInfo.Defs[id]; field != nil { |
| 215 | rename(field) |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // Rename each package-level object. |
| 224 | scope := pkg.Types.Scope() |
| 225 | for _, name := range scope.Names() { |
| 226 | rename(scope.Lookup(name)) |
| 227 | } |
| 228 | |
| 229 | var out bytes.Buffer |
| 230 | if buildTags != "" { |
| 231 | fmt.Fprintf(&out, "//go:build %s\n", buildTags) |
| 232 | fmt.Fprintf(&out, "// +build %s\n\n", buildTags) |
| 233 | } |
| 234 | |
| 235 | fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n") |
| 236 | if *outputFile != "" && buildTags == "" { |
| 237 | fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(quoteArgs(os.Args[1:]), " ")) |
| 238 | } else { |
| 239 | fmt.Fprintf(&out, "// $ bundle %s\n", strings.Join(os.Args[1:], " ")) |
| 240 | } |
| 241 | fmt.Fprintf(&out, "\n") |
| 242 | |
| 243 | // Concatenate package comments from all files... |
| 244 | for _, f := range pkg.Syntax { |
| 245 | if doc := f.Doc.Text(); strings.TrimSpace(doc) != "" { |
| 246 | for _, line := range strings.Split(doc, "\n") { |
| 247 | fmt.Fprintf(&out, "// %s\n", line) |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | // ...but don't let them become the actual package comment. |
| 252 | fmt.Fprintln(&out) |
| 253 | |
| 254 | fmt.Fprintf(&out, "package %s\n\n", dstpkg) |
| 255 | |
| 256 | // BUG(adonovan,shurcooL): bundle may generate incorrect code |
| 257 | // due to shadowing between identifiers and imported package names. |
| 258 | // |
| 259 | // The generated code will either fail to compile or |
| 260 | // (unlikely) compile successfully but have different behavior |
| 261 | // than the original package. The risk of this happening is higher |
| 262 | // when the original package has renamed imports (they're typically |
| 263 | // renamed in order to resolve a shadow inside that particular .go file). |
| 264 | |
| 265 | // TODO(adonovan,shurcooL): |
| 266 | // - detect shadowing issues, and either return error or resolve them |
| 267 | // - preserve comments from the original import declarations. |
| 268 | |
| 269 | // pkgStd and pkgExt are sets of printed import specs. This is done |
| 270 | // to deduplicate instances of the same import name and path. |
| 271 | var pkgStd = make(map[string]bool) |
| 272 | var pkgExt = make(map[string]bool) |
| 273 | for _, f := range pkg.Syntax { |
| 274 | for _, imp := range f.Imports { |
| 275 | path, err := strconv.Unquote(imp.Path.Value) |
| 276 | if err != nil { |
| 277 | log.Fatalf("invalid import path string: %v", err) // Shouldn't happen here since packages.Load succeeded. |
| 278 | } |
| 279 | if path == dst { |
| 280 | continue |
| 281 | } |
| 282 | if newPath, ok := importMap[path]; ok { |
| 283 | path = newPath |
| 284 | } |
| 285 | |
| 286 | var name string |
| 287 | if imp.Name != nil { |
| 288 | name = imp.Name.Name |
| 289 | } |
| 290 | spec := fmt.Sprintf("%s %q", name, path) |
| 291 | if isStandardImportPath(path) { |
| 292 | pkgStd[spec] = true |
| 293 | } else { |
| 294 | pkgExt[spec] = true |
| 295 | } |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | // Print a single declaration that imports all necessary packages. |
| 300 | fmt.Fprintln(&out, "import (") |
| 301 | for p := range pkgStd { |
| 302 | fmt.Fprintf(&out, "\t%s\n", p) |
| 303 | } |
| 304 | if len(pkgExt) > 0 { |
| 305 | fmt.Fprintln(&out) |
| 306 | } |
| 307 | for p := range pkgExt { |
| 308 | fmt.Fprintf(&out, "\t%s\n", p) |
| 309 | } |
| 310 | fmt.Fprint(&out, ")\n\n") |
| 311 | |
| 312 | // Modify and print each file. |
| 313 | for _, f := range pkg.Syntax { |
| 314 | // Update renamed identifiers. |
| 315 | for id, obj := range pkg.TypesInfo.Defs { |
| 316 | if objsToUpdate[obj] { |
| 317 | id.Name = prefix + obj.Name() |
| 318 | } |
| 319 | } |
| 320 | for id, obj := range pkg.TypesInfo.Uses { |
| 321 | if objsToUpdate[obj] { |
| 322 | id.Name = prefix + obj.Name() |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | // For each qualified identifier that refers to the |
| 327 | // destination package, remove the qualifier. |
| 328 | // The "@@@." strings are removed in postprocessing. |
| 329 | ast.Inspect(f, func(n ast.Node) bool { |
| 330 | if sel, ok := n.(*ast.SelectorExpr); ok { |
| 331 | if id, ok := sel.X.(*ast.Ident); ok { |
| 332 | if obj, ok := pkg.TypesInfo.Uses[id].(*types.PkgName); ok { |
| 333 | if obj.Imported().Path() == dst { |
| 334 | id.Name = "@@@" |
| 335 | } |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | return true |
| 340 | }) |
| 341 | |
| 342 | last := f.Package |
| 343 | if len(f.Imports) > 0 { |
| 344 | imp := f.Imports[len(f.Imports)-1] |
| 345 | last = imp.End() |
| 346 | if imp.Comment != nil { |
| 347 | if e := imp.Comment.End(); e > last { |
| 348 | last = e |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | // Pretty-print package-level declarations. |
| 354 | // but no package or import declarations. |
| 355 | var buf bytes.Buffer |
| 356 | for _, decl := range f.Decls { |
| 357 | if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT { |
| 358 | continue |
| 359 | } |
| 360 | |
| 361 | beg, end := sourceRange(decl) |
| 362 | |
| 363 | printComments(&out, f.Comments, last, beg) |
| 364 | |
| 365 | buf.Reset() |
| 366 | format.Node(&buf, pkg.Fset, &printer.CommentedNode{Node: decl, Comments: f.Comments}) |
| 367 | // Remove each "@@@." in the output. |
| 368 | // TODO(adonovan): not hygienic. |
| 369 | out.Write(bytes.Replace(buf.Bytes(), []byte("@@@."), nil, -1)) |
| 370 | |
| 371 | last = printSameLineComment(&out, f.Comments, pkg.Fset, end) |
| 372 | |
| 373 | out.WriteString("\n\n") |
| 374 | } |
| 375 | |
| 376 | printLastComments(&out, f.Comments, last) |
| 377 | } |
| 378 | |
| 379 | // Now format the entire thing. |
| 380 | result, err := format.Source(out.Bytes()) |
| 381 | if err != nil { |
| 382 | log.Fatalf("formatting failed: %v", err) |
| 383 | } |
| 384 | |
| 385 | return result, nil |
| 386 | } |
| 387 | |
| 388 | // sourceRange returns the [beg, end) interval of source code |
| 389 | // belonging to decl (incl. associated comments). |
| 390 | func sourceRange(decl ast.Decl) (beg, end token.Pos) { |
| 391 | beg = decl.Pos() |
| 392 | end = decl.End() |
| 393 | |
| 394 | var doc, com *ast.CommentGroup |
| 395 | |
| 396 | switch d := decl.(type) { |
| 397 | case *ast.GenDecl: |
| 398 | doc = d.Doc |
| 399 | if len(d.Specs) > 0 { |
| 400 | switch spec := d.Specs[len(d.Specs)-1].(type) { |
| 401 | case *ast.ValueSpec: |
| 402 | com = spec.Comment |
| 403 | case *ast.TypeSpec: |
| 404 | com = spec.Comment |
| 405 | } |
| 406 | } |
| 407 | case *ast.FuncDecl: |
| 408 | doc = d.Doc |
| 409 | } |
| 410 | |
| 411 | if doc != nil { |
| 412 | beg = doc.Pos() |
| 413 | } |
| 414 | if com != nil && com.End() > end { |
| 415 | end = com.End() |
| 416 | } |
| 417 | |
| 418 | return beg, end |
| 419 | } |
| 420 | |
| 421 | func printComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos, end token.Pos) { |
| 422 | for _, cg := range comments { |
| 423 | if pos <= cg.Pos() && cg.Pos() < end { |
| 424 | for _, c := range cg.List { |
| 425 | fmt.Fprintln(out, c.Text) |
| 426 | } |
| 427 | fmt.Fprintln(out) |
| 428 | } |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | const infinity = 1 << 30 |
| 433 | |
| 434 | func printLastComments(out *bytes.Buffer, comments []*ast.CommentGroup, pos token.Pos) { |
| 435 | printComments(out, comments, pos, infinity) |
| 436 | } |
| 437 | |
| 438 | func printSameLineComment(out *bytes.Buffer, comments []*ast.CommentGroup, fset *token.FileSet, pos token.Pos) token.Pos { |
| 439 | tf := fset.File(pos) |
| 440 | for _, cg := range comments { |
| 441 | if pos <= cg.Pos() && tf.Line(cg.Pos()) == tf.Line(pos) { |
| 442 | for _, c := range cg.List { |
| 443 | fmt.Fprintln(out, c.Text) |
| 444 | } |
| 445 | return cg.End() |
| 446 | } |
| 447 | } |
| 448 | return pos |
| 449 | } |
| 450 | |
| 451 | func quoteArgs(ss []string) []string { |
| 452 | // From go help generate: |
| 453 | // |
| 454 | // > The arguments to the directive are space-separated tokens or |
| 455 | // > double-quoted strings passed to the generator as individual |
| 456 | // > arguments when it is run. |
| 457 | // |
| 458 | // > Quoted strings use Go syntax and are evaluated before execution; a |
| 459 | // > quoted string appears as a single argument to the generator. |
| 460 | // |
| 461 | var qs []string |
| 462 | for _, s := range ss { |
| 463 | if s == "" || containsSpace(s) { |
| 464 | s = strconv.Quote(s) |
| 465 | } |
| 466 | qs = append(qs, s) |
| 467 | } |
| 468 | return qs |
| 469 | } |
| 470 | |
| 471 | func containsSpace(s string) bool { |
| 472 | for _, r := range s { |
| 473 | if unicode.IsSpace(r) { |
| 474 | return true |
| 475 | } |
| 476 | } |
| 477 | return false |
| 478 | } |
| 479 | |
| 480 | type flagFunc func(string) |
| 481 | |
| 482 | func (f flagFunc) Set(s string) error { |
| 483 | f(s) |
| 484 | return nil |
| 485 | } |
| 486 | |
| 487 | func (f flagFunc) String() string { return "" } |
| 488 |
Members