├── .gitignore ├── testdata └── cram │ ├── .gitignore │ ├── fakehttp-otherdir-assets.go │ ├── cat.t │ ├── cat-xyzzy.go │ ├── var.t │ ├── wrap.t │ ├── cat.go │ ├── bad-no-wrap.t │ ├── bad-var.t │ ├── var-underscore.t │ ├── bad-filename-base.t │ ├── package.t │ ├── dev.t │ ├── bad-empty.t │ ├── dev-otherdir.t │ ├── bad-filename.t │ ├── fakehttp-otherdir.go │ ├── fakehttp.go │ ├── auxiliary.t │ └── help.t ├── example ├── .gitignore ├── index.html └── main.go ├── go.mod ├── asset_nodev.go ├── asset_nodev.go.gen.go ├── go.sum ├── asset_dev.go ├── cram_test.go ├── asset_dev.go.gen.go ├── LICENSE ├── README.md └── becky.go /.gitignore: -------------------------------------------------------------------------------- 1 | /becky 2 | -------------------------------------------------------------------------------- /testdata/cram/.gitignore: -------------------------------------------------------------------------------- 1 | /*.t.err 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /example 2 | *.gen.go 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tv42/becky 2 | 3 | go 1.12 4 | 5 | require golang.org/x/tools v0.0.0-20190703212419-2214986f1668 6 | -------------------------------------------------------------------------------- /testdata/cram/fakehttp-otherdir-assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | //go:generate go run github.com/tv42/becky -var=Greeting greeting.txt 4 | 5 | func txt(a asset) asset { 6 | return a 7 | } 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Hello, world!
9 | 10 | 11 | -------------------------------------------------------------------------------- /testdata/cram/cat.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ cp -- "$TESTDIR/cat.go" . 6 | $ echo Hello, world >greeting.txt 7 | $ go generate 8 | $ go build -o cat 9 | $ ./cat 10 | Hello, world 11 | -------------------------------------------------------------------------------- /testdata/cram/cat-xyzzy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func txt(a asset) string { 10 | return a.Content 11 | } 12 | 13 | func main() { 14 | log.SetFlags(0) 15 | log.SetPrefix("cat-xyzzy.go: ") 16 | 17 | if _, err := io.WriteString(os.Stdout, xyzzy); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/cram/var.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ cp -- "$TESTDIR/cat-xyzzy.go" . 6 | $ echo Hello, world >greeting.txt 7 | $ go run github.com/tv42/becky -var=xyzzy greeting.txt 8 | $ go build -o cat-xyzzy 9 | $ ./cat-xyzzy 10 | Hello, world 11 | -------------------------------------------------------------------------------- /testdata/cram/wrap.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo package main >main.go 6 | $ echo Hello, world >greeting.txt 7 | $ go run github.com/tv42/becky -wrap=xyzzy greeting.txt 8 | $ grep ^var greeting.txt.gen.go | cut -d'(' -f1 9 | var greeting = xyzzy 10 | -------------------------------------------------------------------------------- /testdata/cram/cat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | //go:generate go run github.com/tv42/becky greeting.txt 10 | 11 | func txt(a asset) string { 12 | return a.Content 13 | } 14 | 15 | func main() { 16 | log.SetFlags(0) 17 | log.SetPrefix("cat.go: ") 18 | 19 | if _, err := io.WriteString(os.Stdout, greeting); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testdata/cram/bad-no-wrap.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo package main >main.go 6 | $ echo Hello, world >no-extension 7 | $ go run github.com/tv42/becky -var=xyzzy no-extension 8 | becky: files without extension need -wrap: no-extension 9 | exit status 1 10 | [1] 11 | -------------------------------------------------------------------------------- /testdata/cram/bad-var.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo package main >main.go 6 | $ echo Hello, world >greeting.txt 7 | $ go run github.com/tv42/becky -var=not-a-go-identifier greeting.txt 8 | becky: not a valid go identifier: not-a-go-identifier 9 | exit status 1 10 | [1] 11 | -------------------------------------------------------------------------------- /testdata/cram/var-underscore.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo package main >main.go 6 | $ echo foo >one.txt 7 | $ echo bar >two.txt 8 | $ go run github.com/tv42/becky -var=_ one.txt two.txt 9 | $ grep ^var one.txt.gen.go two.txt.gen.go | cut -d= -f1 10 | one.txt.gen.go:var _ 11 | two.txt.gen.go:var _ 12 | -------------------------------------------------------------------------------- /testdata/cram/bad-filename-base.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | Empty file basenames cannot be used as variable name. 6 | 7 | $ echo package main >main.go 8 | $ echo Hello, world >.txt 9 | $ go run github.com/tv42/becky .txt 10 | becky: cannot use empty file basename as identifier: .txt 11 | exit status 1 12 | [1] 13 | -------------------------------------------------------------------------------- /testdata/cram/package.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo package xyzzy >xyzzy.go 6 | $ echo Hello, world >greeting.txt 7 | $ go run github.com/tv42/becky greeting.txt 8 | $ grep ^package *.gen.go | sort 9 | asset_dev.gen.go:package xyzzy 10 | asset_nodev.gen.go:package xyzzy 11 | greeting.txt.gen.go:package xyzzy 12 | -------------------------------------------------------------------------------- /asset_nodev.go: -------------------------------------------------------------------------------- 1 | // +build !dev 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type asset struct { 12 | Name string 13 | Content string 14 | etag string 15 | } 16 | 17 | func (a asset) ServeHTTP(w http.ResponseWriter, req *http.Request) { 18 | if a.etag != "" && w.Header().Get("ETag") == "" { 19 | w.Header().Set("ETag", a.etag) 20 | } 21 | body := strings.NewReader(a.Content) 22 | http.ServeContent(w, req, a.Name, time.Time{}, body) 23 | } 24 | -------------------------------------------------------------------------------- /testdata/cram/dev.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ cp -- "$TESTDIR/fakehttp.go" . 6 | $ echo Hello, world >greeting.txt 7 | $ go generate 8 | $ go build -tags=dev -o fakehttp 9 | $ ./fakehttp 10 | Hello, world 11 | $ rm greeting.txt 12 | $ ./fakehttp 13 | fakehttp.go: bad response: 500: open .*/greeting.txt: no such file or directory (re) 14 | [1] 15 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | //go:generate go run github.com/tv42/becky index.html 10 | 11 | type HTML struct { 12 | asset 13 | } 14 | 15 | func html(a asset) HTML { 16 | return HTML{a} 17 | } 18 | 19 | func main() { 20 | l, err := net.Listen("tcp", "localhost:0") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | log.Printf("Serving at http://%s/", l.Addr()) 25 | http.Handle("/", index) 26 | if err := http.Serve(l, nil); err != nil { 27 | log.Fatal(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /testdata/cram/bad-empty.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ echo Hello, world >greeting.txt 6 | $ go run github.com/tv42/becky greeting.txt 7 | becky: cannot load package: go [list -e -json -compiled=false -test=false -export=false -deps=false -find=true -- .]: exit status 1: build .: cannot find module for path . 8 | exit status 1 9 | [1] 10 | $ find -type f -printf '%P\n' | sort 11 | go.mod 12 | go.sum 13 | greeting.txt 14 | -------------------------------------------------------------------------------- /asset_nodev.go.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/tv42/becky -- DO NOT EDIT. 2 | 3 | package main 4 | 5 | var assetNoDev = asset(asset{Name: "asset_nodev.go", Content: "" + 6 | "// +build !dev\n\npackage main\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype asset struct {\n\tName string\n\tContent string\n\tetag string\n}\n\nfunc (a asset) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tif a.etag != \"\" && w.Header().Get(\"ETag\") == \"\" {\n\t\tw.Header().Set(\"ETag\", a.etag)\n\t}\n\tbody := strings.NewReader(a.Content)\n\thttp.ServeContent(w, req, a.Name, time.Time{}, body)\n}\n" + 7 | "", etag: `"pGCgphv16Ds="`}) 8 | -------------------------------------------------------------------------------- /testdata/cram/dev-otherdir.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ cp -- "$TESTDIR/fakehttp-otherdir.go" . 6 | $ mkdir assets 7 | $ echo Hello, world >assets/greeting.txt 8 | $ cp -- "$TESTDIR/fakehttp-otherdir-assets.go" assets/assets.go 9 | $ go generate ./assets 10 | $ go build -tags=dev -o fakehttp 11 | $ ./fakehttp 12 | Hello, world 13 | $ rm assets/greeting.txt 14 | $ ./fakehttp 15 | fakehttp.go: bad response: 500: open .*/assets/greeting.txt: no such file or directory (re) 16 | [1] 17 | -------------------------------------------------------------------------------- /testdata/cram/bad-filename.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | Files with basenames that are not Go identifiers are errors, unless 6 | the `-var` flag is used. 7 | 8 | $ cp -- "$TESTDIR/cat-xyzzy.go" . 9 | $ echo Hello, world >not-a-go-identifier.txt 10 | $ go run github.com/tv42/becky not-a-go-identifier.txt 11 | becky: not a valid go identifier: not-a-go-identifier 12 | exit status 1 13 | [1] 14 | $ go run github.com/tv42/becky -var=xyzzy not-a-go-identifier.txt 15 | $ go build -o cat-xyzzy 16 | $ ./cat-xyzzy 17 | Hello, world 18 | -------------------------------------------------------------------------------- /testdata/cram/fakehttp-otherdir.go: -------------------------------------------------------------------------------- 1 | package main // import "example.com/myproject" 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | 10 | "example.com/myproject/assets" 11 | ) 12 | 13 | func main() { 14 | log.SetFlags(0) 15 | log.SetPrefix("fakehttp.go: ") 16 | 17 | rec := httptest.NewRecorder() 18 | rec.Body = &bytes.Buffer{} 19 | req, err := http.NewRequest("GET", "/", nil) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | assets.Greeting.ServeHTTP(rec, req) 24 | if rec.Code != 200 { 25 | log.Fatalf("bad response: %v: %s", rec.Code, rec.Body.Bytes()) 26 | } 27 | if _, err := os.Stdout.Write(rec.Body.Bytes()); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 3 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 4 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 5 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 6 | golang.org/x/tools v0.0.0-20190703212419-2214986f1668 h1:3LJOYcj2ObWSZJXX21oGIIPv5SaOoi5JkzQTWnCXRhg= 7 | golang.org/x/tools v0.0.0-20190703212419-2214986f1668/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 8 | -------------------------------------------------------------------------------- /testdata/cram/fakehttp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | ) 10 | 11 | //go:generate go run github.com/tv42/becky greeting.txt 12 | 13 | func txt(a asset) asset { 14 | return a 15 | } 16 | 17 | func main() { 18 | log.SetFlags(0) 19 | log.SetPrefix("fakehttp.go: ") 20 | 21 | rec := httptest.NewRecorder() 22 | rec.Body = &bytes.Buffer{} 23 | req, err := http.NewRequest("GET", "/", nil) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | greeting.ServeHTTP(rec, req) 28 | if rec.Code != 200 { 29 | log.Fatalf("bad response: %v: %s", rec.Code, rec.Body.Bytes()) 30 | } 31 | if _, err := os.Stdout.Write(rec.Body.Bytes()); err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testdata/cram/auxiliary.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | Using `-lib=false` disables creating `asset_*.gen.go` files: 6 | 7 | $ echo package main >main.go 8 | $ echo Hello, world >greeting.txt 9 | $ go run github.com/tv42/becky -lib=false greeting.txt 10 | $ find -type f -printf '%P\n' | sort 11 | go.mod 12 | go.sum 13 | greeting.txt 14 | greeting.txt.gen.go 15 | main.go 16 | 17 | Leaving it out gets them created: 18 | 19 | $ go run github.com/tv42/becky greeting.txt 20 | $ find -type f -printf '%P\n' | sort 21 | asset_dev.gen.go 22 | asset_nodev.gen.go 23 | go.mod 24 | go.sum 25 | greeting.txt 26 | greeting.txt.gen.go 27 | main.go 28 | -------------------------------------------------------------------------------- /asset_dev.go: -------------------------------------------------------------------------------- 1 | // +build dev 2 | 3 | package main 4 | 5 | import ( 6 | "go/build" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | type asset struct { 14 | Name string 15 | Content string 16 | etag string 17 | } 18 | 19 | func (a asset) importPath() string { 20 | // filled at code gen time 21 | return "{{.ImportPath}}" 22 | } 23 | 24 | func (a asset) Open() (*os.File, error) { 25 | path := a.importPath() 26 | pkg, err := build.Import(path, ".", build.FindOnly) 27 | if err != nil { 28 | return nil, err 29 | } 30 | p := filepath.Join(pkg.Dir, a.Name) 31 | return os.Open(p) 32 | } 33 | 34 | func (a asset) ServeHTTP(w http.ResponseWriter, req *http.Request) { 35 | body, err := a.Open() 36 | if err != nil { 37 | // show the os.Open message, with paths and all, but this only 38 | // happens in dev mode. 39 | http.Error(w, err.Error(), http.StatusInternalServerError) 40 | return 41 | } 42 | defer body.Close() 43 | http.ServeContent(w, req, a.Name, time.Time{}, body) 44 | } 45 | -------------------------------------------------------------------------------- /cram_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os/exec" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func runCram(cram, test string) func(t *testing.T) { 12 | fn := func(t *testing.T) { 13 | buf := new(bytes.Buffer) 14 | cmd := exec.Command(cram, test) 15 | cmd.Stdout = buf 16 | cmd.Stderr = buf 17 | if err := cmd.Run(); err != nil { 18 | t.Log(buf.String()) 19 | t.Fatalf("cram: %v", err) 20 | } 21 | } 22 | return fn 23 | } 24 | 25 | func TestCram(t *testing.T) { 26 | cram, err := exec.LookPath("cram") 27 | if err != nil { 28 | t.Fatalf("cannot find cram test runner: %v", err) 29 | } 30 | const dir = "testdata/cram" 31 | fis, err := ioutil.ReadDir(dir) 32 | if err != nil { 33 | t.Fatalf("cannot list cram tests: %v", err) 34 | } 35 | for _, fi := range fis { 36 | name := fi.Name() 37 | ext := filepath.Ext(name) 38 | if ext != ".t" { 39 | continue 40 | } 41 | base := name[:len(name)-len(ext)] 42 | t.Run(base, runCram(cram, filepath.Join(dir, name))) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /asset_dev.go.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/tv42/becky -- DO NOT EDIT. 2 | 3 | package main 4 | 5 | var assetDev = asset(asset{Name: "asset_dev.go", Content: "" + 6 | "// +build dev\n\npackage main\n\nimport (\n\t\"go/build\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\ntype asset struct {\n\tName string\n\tContent string\n\tetag string\n}\n\nfunc (a asset) importPath() string {\n\t// filled at code gen time\n\treturn \"{{.ImportPath}}\"\n}\n\nfunc (a asset) Open() (*os.File, error) {\n\tpath := a.importPath()\n\tpkg, err := build.Import(path, \".\", build.FindOnly)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp := filepath.Join(pkg.Dir, a.Name)\n\treturn os.Open(p)\n}\n\nfunc (a asset) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tbody, err := a.Open()\n\tif err != nil {\n\t\t// show the os.Open message, with paths and all, but this only\n\t\t// happens in dev mode.\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer body.Close()\n\thttp.ServeContent(w, req, a.Name, time.Time{}, body)\n}\n" + 7 | "", etag: `"+RnCzXLmOR4="`}) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Tommi Virtanen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /testdata/cram/help.t: -------------------------------------------------------------------------------- 1 | $ go mod init example.com/myproject 2 | go: creating new go.mod: module example.com/myproject 3 | $ go mod edit -replace=github.com/tv42/becky="$TESTDIR/../.." 4 | 5 | $ go run github.com/tv42/becky 6 | Usage: 7 | becky [OPTS] FILE.. 8 | 9 | Creates files FILE.gen.go and asset_*.gen.go 10 | 11 | Options: 12 | -lib 13 | \tgenerate asset_*.gen.go files defining the asset type (default true) (esc) 14 | -var string 15 | \tvariable name to use, "_" to ignore (default: file basename without extension) (esc) 16 | -wrap string 17 | \twrapper function or type (default: filename extension) (esc) 18 | exit status 2 19 | [1] 20 | 21 | $ go run github.com/tv42/becky -help 22 | Usage: 23 | becky [OPTS] FILE.. 24 | 25 | Creates files FILE.gen.go and asset_*.gen.go 26 | 27 | Options: 28 | -lib 29 | \tgenerate asset_*.gen.go files defining the asset type (default true) (esc) 30 | -var string 31 | \tvariable name to use, "_" to ignore (default: file basename without extension) (esc) 32 | -wrap string 33 | \twrapper function or type (default: filename extension) (esc) 34 | exit status 2 35 | [1] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Becky -- Go asset embedding for use with `go generate` 2 | 3 | Becky embeds assets as string literals in Go source. 4 | 5 | **OBSOLETE** Becky has been made unnecessary by the [`embed` package](https://golang.org/pkg/embed) in [Go 1.16](https://blog.golang.org/go1.16). It will not be developed further. 6 | 7 | 8 | ## Usage 9 | 10 | Use becky via `tools.go`, see 11 | https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 12 | 13 | For example, in your project create `tools.go` that has: 14 | 15 | ``` 16 | // +build tools 17 | 18 | package tools 19 | 20 | import ( 21 | _ "github.com/tv42/becky" 22 | ) 23 | ``` 24 | 25 | And then near where you use the asset, put 26 | 27 | ``` go 28 | //go:generate go run github.com/tv42/becky index.html 29 | ``` 30 | 31 | and run 32 | 33 | ``` console 34 | $ go generate 35 | ``` 36 | 37 | This will create new files, named `*.gen.go`. You should add those 38 | into your version control system, to ensure `go get` works for others. 39 | 40 | You can pass multiple asset files at once, or repeat the `go:generate` 41 | line. 42 | 43 | 44 | ## Variable name 45 | 46 | The generated files declare variables that now contain your assets. 47 | Given the above `index.html`, the variable will be named `index`. 48 | 49 | You can override the name with `-var=NAME`, or skip it with `-var=_` 50 | and use side effects in your wrapper function (discussed later). 51 | 52 | The asset will be an value of `type asset` (this code is 53 | autogenerated, you don't need to type it in): 54 | 55 | ``` go 56 | type asset struct { 57 | Name string 58 | Content string 59 | ... 60 | } 61 | ``` 62 | 63 | Name has the original filename, as a hint for `Content-Type` 64 | selection. 65 | 66 | 67 | ## Wrapper 68 | 69 | For most uses, an `asset` value needs to be given application or file 70 | type specific functionality. To make this easy, the asset value will 71 | be passed to a function caller *wrapper*. **You** need to write these 72 | wrapper functions. 73 | 74 | The name of the default wrapper is the (final) extension of the asset 75 | filename. For `index.html`, that's `html`. You can override the 76 | wrapper with `-wrap=NAME`. 77 | 78 | In your application, you'd do something like 79 | 80 | ``` go 81 | func html(a asset) http.Handler { 82 | return a 83 | } 84 | ``` 85 | 86 | or 87 | 88 | ``` go 89 | func txt(a asset) string { 90 | return a.Content 91 | } 92 | ``` 93 | 94 | or 95 | 96 | ``` go 97 | func tmpl(a asset) *template.Template { 98 | return template.Must(template.New(a.Name).Parse(a.Content)) 99 | } 100 | ``` 101 | 102 | to smartly handle `*.html`, `*.txt` and `*.tmpl` assets. Feel free 103 | to pass the fields of `asset` to a factory function or type that 104 | matches what you need, or use the `asset`, whatever suits your 105 | project. 106 | 107 | 108 | ## HTTP 109 | 110 | Type `asset` implements `http.Handler`, including `ETag` cache 111 | validation. It uses `http.ServeContent` which will set `Content-Type` 112 | from the file name or content, and handle `Range` requests. 113 | 114 | 115 | ## Code generation speed 116 | 117 | If repeated calls to `go run github.com/tv42/becky` are too slow, you 118 | can build it once and run from there: 119 | 120 | ``` 121 | //go:generate go build github.com/tv42/becky 122 | //go:generate ./becky index.html 123 | ``` 124 | 125 | 126 | ## Build speed 127 | 128 | `gc`, the Go compiler, can slow down with large source files. As e.g. 129 | image assets can get big, this can start to slow down your builds. The 130 | mechanism used for embedding has been chosen to be the most efficient 131 | available. 132 | 133 | Embedding a 10MB asset (creating a 28MB Go source file) takes <1 134 | second to generate the code and about 1 second for every compilation. 135 | 136 | You can minimize the number of times assets need to be compiled by 137 | putting them in a different package that updates less often than most 138 | of your source. 139 | 140 | ## Development mode 141 | 142 | If you build your application with `-tags dev`, `asset.ServeHTTP` will 143 | reload the asset from disk on every request, and not use the embedded 144 | copy. This makes editing HTML, CSS and such more convenient. 145 | -------------------------------------------------------------------------------- /becky.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "hash/fnv" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | "text/template" 18 | "unicode" 19 | 20 | "golang.org/x/tools/go/packages" 21 | ) 22 | 23 | //go:generate go run . -lib=false -var=assetDev -wrap=asset -- asset_dev.go 24 | //go:generate go run . -lib=false -var=assetNoDev -wrap=asset -- asset_nodev.go 25 | 26 | var ( 27 | flagVar = flag.String("var", "", "variable name to use, \"_\" to ignore (default: file basename without extension)") 28 | flagWrap = flag.String("wrap", "", "wrapper function or type (default: filename extension)") 29 | flagLib = flag.Bool("lib", true, "generate asset_*.gen.go files defining the asset type") 30 | ) 31 | 32 | const prog = "becky" 33 | 34 | func usage() { 35 | fmt.Fprintf(os.Stderr, "Usage:\n") 36 | fmt.Fprintf(os.Stderr, " %s [OPTS] FILE..\n", prog) 37 | fmt.Fprintf(os.Stderr, "\n") 38 | fmt.Fprintf(os.Stderr, "Creates files FILE.gen.go and asset_*.gen.go\n") 39 | fmt.Fprintf(os.Stderr, "\n") 40 | fmt.Fprintf(os.Stderr, "Options:\n") 41 | flag.PrintDefaults() 42 | } 43 | 44 | func main() { 45 | log.SetFlags(0) 46 | log.SetPrefix(prog + ": ") 47 | 48 | flag.Usage = usage 49 | flag.Parse() 50 | if flag.NArg() == 0 { 51 | flag.Usage() 52 | os.Exit(2) 53 | } 54 | 55 | if flag.NArg() > 1 && *flagVar != "" && *flagVar != "_" { 56 | log.Fatal("cannot combine -var with multiple files") 57 | } 58 | 59 | // Look up package names and import paths only once. Every dir 60 | // here is also guaranteed to have the auxiliary files already, to 61 | // avoid repeating work. 62 | packages := map[string]*packages.Package{} 63 | 64 | for _, filename := range flag.Args() { 65 | dir, base := filepath.Split(filename) 66 | if dir == "" { 67 | dir = "." 68 | } 69 | 70 | pkg, err := getPkg(packages, dir) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | variable := *flagVar 76 | if variable == "" { 77 | variable = strings.SplitN(base, ".", 2)[0] 78 | if variable == "" { 79 | log.Fatalf("cannot use empty file basename as identifier: %s", filename) 80 | } 81 | } 82 | if !isIdentifier(variable) { 83 | log.Fatalf("not a valid go identifier: %s", variable) 84 | } 85 | 86 | wrap := *flagWrap 87 | if wrap == "" { 88 | wrap = filepath.Ext(base) 89 | if wrap == "" { 90 | log.Fatalf("files without extension need -wrap: %s", filename) 91 | } 92 | // remove the dot 93 | wrap = wrap[1:] 94 | } 95 | 96 | if err := process(filename, pkg.Name, variable, wrap); err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | } 101 | } 102 | 103 | // autogen writes a warning that the file has been generated automatically. 104 | func autogen(w io.Writer) error { 105 | // broken into parts here so grep won't find it 106 | const warning = "// Code " /* but not this file */ + "generated by github.com/tv42/becky -- DO N" + "OT EDIT.\n\n" 107 | _, err := io.WriteString(w, warning) 108 | return err 109 | } 110 | 111 | func process(filename, pkg, variable, wrap string) error { 112 | src, err := os.Open(filename) 113 | if err != nil { 114 | return err 115 | } 116 | defer src.Close() 117 | 118 | tmp, err := ioutil.TempFile(filepath.Dir(filename), ".tmp.asset-") 119 | if err != nil { 120 | return err 121 | } 122 | defer func() { 123 | if tmp != nil { 124 | _ = os.Remove(tmp.Name()) 125 | } 126 | }() 127 | defer tmp.Close() 128 | 129 | in := bufio.NewReader(src) 130 | out := bufio.NewWriter(tmp) 131 | 132 | if err := autogen(out); err != nil { 133 | return err 134 | } 135 | if _, err := fmt.Fprintf(out, "package %s\n\n", pkg); err != nil { 136 | return err 137 | } 138 | if err := embed(variable, wrap, filepath.Base(filename), in, out); err != nil { 139 | return err 140 | } 141 | if err := out.Flush(); err != nil { 142 | return err 143 | } 144 | if err := tmp.Close(); err != nil { 145 | return err 146 | } 147 | gen := filename + ".gen.go" 148 | if err := os.Rename(tmp.Name(), gen); err != nil { 149 | return err 150 | } 151 | tmp = nil 152 | return nil 153 | } 154 | 155 | func embed(variable, wrap, filename string, in io.Reader, out io.Writer) error { 156 | h := fnv.New64a() 157 | r := io.TeeReader(in, h) 158 | _, err := fmt.Fprintf(out, "var %s = %s(asset{Name: %q, Content: \"\" +\n", 159 | variable, wrap, filename) 160 | if err != nil { 161 | return err 162 | } 163 | buf := make([]byte, 1*1024*1024) 164 | eof := false 165 | for !eof { 166 | n, err := io.ReadFull(r, buf) 167 | switch err { 168 | case io.EOF, io.ErrUnexpectedEOF: 169 | eof = true 170 | case nil: 171 | // nothing 172 | default: 173 | return err 174 | } 175 | if n == 0 { 176 | continue 177 | } 178 | s := string(buf[:n]) 179 | s = strconv.QuoteToASCII(s) 180 | s = "\t" + s + " +\n" 181 | if _, err := io.WriteString(out, s); err != nil { 182 | return err 183 | } 184 | } 185 | etag := `"` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `"` 186 | if _, err := fmt.Fprintf(out, "\t\"\", etag: %#q})\n", etag); err != nil { 187 | return err 188 | } 189 | return nil 190 | } 191 | 192 | func getPkg(packages map[string]*packages.Package, dir string) (*packages.Package, error) { 193 | if pkg, found := packages[dir]; found { 194 | return pkg, nil 195 | } 196 | 197 | pkg, err := loadPkg(dir) 198 | if err != nil { 199 | return nil, err 200 | } 201 | if *flagLib { 202 | if err := auxiliary(dir, pkg.PkgPath, pkg.Name); err != nil { 203 | return nil, err 204 | } 205 | } 206 | packages[dir] = pkg 207 | return pkg, nil 208 | } 209 | 210 | func loadPkg(dir string) (*packages.Package, error) { 211 | cfg := &packages.Config{ 212 | Mode: packages.NeedName, 213 | } 214 | pkgs, err := packages.Load(cfg, "pattern="+dir) 215 | if err != nil { 216 | return nil, fmt.Errorf("cannot load package: %w", err) 217 | } 218 | if len(pkgs) != 1 { 219 | return nil, errors.New("packages.Load found more than one package") 220 | } 221 | pkg := pkgs[0] 222 | switch len(pkg.Errors) { 223 | case 0: 224 | return pkg, nil 225 | case 1: 226 | return nil, fmt.Errorf("packages.Load: %v", pkg.Errors[0]) 227 | default: 228 | for _, err := range pkg.Errors { 229 | log.Printf("packages.Load: %v", err) 230 | } 231 | return nil, errors.New("packages.Load failed") 232 | } 233 | } 234 | 235 | func auxiliary(dir, imp, pkg string) error { 236 | for filename, tmpl := range map[string]string{ 237 | "asset_dev": assetDev.Content, 238 | "asset_nodev": assetNoDev.Content, 239 | } { 240 | tmpl = strings.Replace(tmpl, "\npackage main\n", "\npackage "+pkg+"\n", 1) 241 | 242 | t, err := template.New("").Parse(tmpl) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | tmp, err := ioutil.TempFile(dir, ".tmp.asset-") 248 | if err != nil { 249 | return err 250 | } 251 | defer func() { 252 | if tmp != nil { 253 | _ = os.Remove(tmp.Name()) 254 | } 255 | }() 256 | defer tmp.Close() 257 | 258 | type data struct { 259 | ImportPath string 260 | } 261 | d := data{ 262 | ImportPath: imp, 263 | } 264 | if err := autogen(tmp); err != nil { 265 | return err 266 | } 267 | if err := t.Execute(tmp, d); err != nil { 268 | return err 269 | } 270 | if err := tmp.Close(); err != nil { 271 | return err 272 | } 273 | gen := filepath.Join(dir, filename+".gen.go") 274 | if err := os.Rename(tmp.Name(), gen); err != nil { 275 | return err 276 | } 277 | tmp = nil 278 | } 279 | return nil 280 | } 281 | 282 | // isIdentifier reports whether the things is a valid Go identifier. 283 | func isIdentifier(s string) bool { 284 | // https://golang.org/ref/spec#Identifiers 285 | // identifier = letter { letter | unicode_digit } . 286 | // letter = unicode_letter | "_" . 287 | // unicode_letter = /* a Unicode code point classified as "Letter" */ . 288 | // unicode_digit = /* a Unicode code point classified as "Number, decimal digit" */ . 289 | 290 | if len(s) == 0 { 291 | return false 292 | } 293 | for i, r := range s { 294 | if i == 0 { 295 | if !unicode.IsLetter(r) && r != '_' { 296 | return false 297 | } 298 | } else { 299 | if !unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r) { 300 | return false 301 | } 302 | } 303 | } 304 | return true 305 | } 306 | --------------------------------------------------------------------------------