diff --git a/README.md b/README.md index 4db2921..2ef84bb 100644 --- a/README.md +++ b/README.md @@ -4,45 +4,10 @@ This tool converts any file into managable Go source code. Useful for embedding binary data into a go program. The file data is optionally gzip compressed before being converted to a raw byte slice. + ### Usage -The simplest invocation is to pass it only the input file name. -The output file and code settings are inferred from this automatically. - - $ go-bindata testdata/gophercolor.png - [w] No output file specified. Using 'testdata/gophercolor.png.go'. - [w] No package name specified. Using 'main'. - [w] No function name specified. Using 'testdata_gophercolor_png'. - -This creates the `testdata/gophercolor.png.go` file which has a package -declaration with name `main` and one function named `testdata_gophercolor_png` with -the following signature: - -```go -func testdata_gophercolor_png() []byte -``` - -You can now simply include the new .go file in your program and call -`testdata_gophercolor_png()` to get the (uncompressed) image data. The function panics -if something went wrong during decompression. See the testdata directory for -example input and output files for various modes. - -Aternatively, you can pipe the input file data into stdin. `go-bindata` will -then spit out the generated Go code to stdout. This does require explicitly -naming the desired function name, as it can not be inferred from the -input data. The package name will still default to 'main'. - - $ cat testdata/gophercolor.png | go-bindata -f gophercolor_png | gofmt - -Invoke the program with the `-h` flag for more options. - -In order to strip off a part of the generated function name, we can use the `-prefix` flag. -In the above example, the input file `testdata/gophercolor.png` yields a function named -`testdata_gophercolor_png`. If we want the `testdata` component to be left out, we invoke -the program as follows: - - $ go-bindata -prefix "testdata/" testdata/gophercolor.png - +TODO ### Lower memory footprint @@ -108,25 +73,6 @@ even increase the size of the data. The default behaviour of the program is to use compression. -### Table of Contents - -With the `-toc` flag, we can have `go-bindata` create a table of contents for all the files -which have been generated by the tool. It does this by first generating a new file named -`bindata-toc.go`. This contains a global map of type `map[string] func() []byte`. It uses the -input filename as the key and the data function as the value. We can use this -to fetch all data for our files, matching a given pattern. - -It then appands an `init` function to each generated file, which simply makes the data -function append itself to the global `bindata` map. - -Once you have compiled your program with all these new files and run it, the map will -be populated by all generated data files. - -**Note**: The `bindata-toc.go` file will not be created when we run in `pipe` mode. -The reason being, that the tool does not write any files at all, as it has no idea -where to save them. The data file is written to `stdout` instead after all. - - #### Table of Contents keys The keys used in the `go_bindata` map, are the same as the input file name passed to `go-bindata`. @@ -148,20 +94,6 @@ Running with the `-prefix` flag, we get: go_bindata["templates/foo.html"] = templates_foo_html -#### bindata-toc.go - -The `bindata-toc.go` file is very simple and looks as follows: - -```go -package $PACKAGENAME - -// Global Table of Contents map. Generated by go-bindata. -// After startup of the program, all generated data files will -// put themselves in this map. The key is the full filename, as -// supplied to go-bindata. -var go_bindata = make(map[string] func() []byte) -``` - #### Build tags With the optional -tags flag, you can specify any go build tags that diff --git a/config.go b/config.go new file mode 100644 index 0000000..c9d7f96 --- /dev/null +++ b/config.go @@ -0,0 +1,92 @@ +// This work is subject to the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication +// license. Its contents can be found at: +// http://creativecommons.org/publicdomain/zero/1.0/ + +package bindata + +// Config defines a set of options for the asset conversion. +type Config struct { + // Tags specify a set of optional build tags, which should be + // included in the generated output. The tags are appended to a + // `// +build` line in the beginning of the output file + // and must follow the build tags syntax specified by the go tool. + Tags []string + + // Input defines the directory path, containing all asset files. + // This may contain sub directories, which will be included in the + // conversion. + Input string + + // Output defines the output directory for the generated code. + Output string + + /* + Prefix defines a path prefix which should be stripped from all + file names when generating the keys in the table of contents. + For example, running without the `-prefix` flag, we get: + + $ go-bindata /path/to/templates + go_bindata["/path/to/templates/foo.html"] = _path_to_templates_foo_html + + Running with the `-prefix` flag, we get: + + $ go-bindata -prefix "/path/to/" /path/to/templates/foo.html + go_bindata["templates/foo.html"] = templates_foo_html + */ + Prefix string + + /* + NoMemCopy will alter the way the output file is generated. + + It will employ a hack that allows us to read the file data directly from + the compiled program's `.rodata` section. This ensures that when we call + call our generated function, we omit unnecessary mem copies. + + The downside of this, is that it requires dependencies on the `reflect` and + `unsafe` packages. These may be restricted on platforms like AppEngine and + thus prevent you from using this mode. + + Another disadvantage is that the byte slice we create, is strictly read-only. + For most use-cases this is not a problem, but if you ever try to alter the + returned byte slice, a runtime panic is thrown. Use this mode only on target + platforms where memory constraints are an issue. + + The default behaviour is to use the old code generation method. This + prevents the two previously mentioned issues, but will employ at least one + extra memcopy and thus increase memory requirements. + + For instance, consider the following two examples: + + This would be the default mode, using an extra memcopy but gives a safe + implementation without dependencies on `reflect` and `unsafe`: + + func myfile() []byte { + return []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a} + } + + Here is the same functionality, but uses the `.rodata` hack. + The byte slice returned from this example can not be written to without + generating a runtime error. + + var _myfile = "\x89\x50\x4e\x47\x0d\x0a\x1a" + + func myfile() []byte { + var empty [0]byte + sx := (*reflect.StringHeader)(unsafe.Pointer(&_myfile)) + b := empty[:] + bx := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bx.Data = sx.Data + bx.Len = len(_myfile) + bx.Cap = bx.Len + return b + } + */ + NoMemCopy bool + + /* + Compress means the assets are GZIP compressed before being turned + into Go code. The generated function will automatically unzip + the file data when called. + */ + Compress bool +} diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..757add6 --- /dev/null +++ b/convert.go @@ -0,0 +1,120 @@ +// This work is subject to the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication +// license. Its contents can be found at: +// http://creativecommons.org/publicdomain/zero/1.0/ + +package bindata + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "unicode" +) + +// ProgressFunc is a callback handler which is fired whenever +// bindata begins translating a new file. +// +// It takes the file path and the current and total file counts. +// This can be used to indicate progress of the conversion. +// +// If this handler returns true, the processing is stopped and +// Translate() returns immediately. +type ProgressFunc func(file string, current, total int) bool + +// Translate reads assets from an input directory, converts them +// to Go code and writes new files to the output directory specified +// in the given configuration. +func Translate(c *Config, pf ProgressFunc) error { + toc := make(map[string]string) + err := findFiles(c.Input, c.Prefix, toc) + + if err != nil { + return err + } + + var current int + for key, value := range toc { + if pf != nil { + current++ + if pf(key, current, len(toc)) { + return nil + } + } + + _ = value + } + + return nil +} + +// fillTOC recursively finds all the file paths in the given directory tree. +// They are added to the given map as keys. Values will be safe function names +// for each file, which will be used when generating the output code. +func findFiles(dir, prefix string, toc map[string]string) error { + if len(prefix) > 0 { + dir, _ = filepath.Abs(dir) + prefix, _ = filepath.Abs(prefix) + } + + fd, err := os.Open(dir) + if err != nil { + return err + } + + defer fd.Close() + + list, err := fd.Readdir(0) + if err != nil { + return err + } + + for _, file := range list { + key := filepath.Join(dir, file.Name()) + + if file.IsDir() { + findFiles(key, prefix, toc) + } else { + if strings.HasPrefix(key, prefix) { + key = key[len(prefix):] + } + + // If we have a leading slash, get rid of it. + if len(key) > 0 && key[0] == '/' { + key = key[1:] + } + + // This shouldn't happen. + if len(key) == 0 { + return fmt.Errorf("Invalid file: %v", filepath.Join(dir, file.Name())) + } + + value := safeFunctionName(key) + toc[key] = value + } + } + + return nil +} + +var regFuncName = regexp.MustCompile(`[^a-zA-Z0-9_]`) + +// safeFunctionName converts the given name into a name +// which qualifies as a valid function identifier. +func safeFunctionName(name string) string { + name = strings.ToLower(name) + name = regFuncName.ReplaceAllString(name, "_") + + // Identifier can't start with a digit. + if unicode.IsDigit(rune(name[0])) { + name = "_" + name + } + + // Get rid of "__" instances for niceness. + for strings.Index(name, "__") > -1 { + name = strings.Replace(name, "__", "_", -1) + } + + return name +} diff --git a/go-bindata/config.go b/go-bindata/config.go deleted file mode 100644 index d5cdaf5..0000000 --- a/go-bindata/config.go +++ /dev/null @@ -1,106 +0,0 @@ -// This work is subject to the CC0 1.0 Universal (CC0 1.0) Public Domain Dedication -// license. Its contents can be found at: -// http://creativecommons.org/publicdomain/zero/1.0/ - -package main - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "strings" -) - -// Config defines command line options. -type Config struct { - Input string // Input directory with assets. - Output string // Output directory for generated code. - Tags []string // Build tags to include in output files. -} - -// NewConfig create s anew, filled configuration instance -// by reading and parsing command line options. -// -// This function exits the program with an error, if -// any of the command line options are incorrect. -func NewConfig() *Config { - var version bool - var tagstr string - - c := new(Config) - - flag.Usage = func() { - fmt.Printf("Usage: %s [options] []\n\n", os.Args[0]) - flag.PrintDefaults() - } - - flag.StringVar(&tagstr, "tags", "", "Comma-separated list of build tags to include.") - flag.BoolVar(&version, "version", false, "Displays version information.") - flag.Parse() - - if version { - fmt.Printf("%s\n", Version()) - os.Exit(0) - } - - // Make sure we have in/output paths. - if flag.NArg() == 0 { - fmt.Fprintf(os.Stderr, "Missing asset directory.\n") - os.Exit(1) - } - - // Test validity of input path. - c.Input = filepath.Clean(flag.Arg(0)) - - stat, err := os.Lstat(c.Input) - if err != nil { - fmt.Fprintf(os.Stderr, "Input path: %v.\n", err) - os.Exit(1) - } - - if !stat.IsDir() { - fmt.Fprintf(os.Stderr, "Input path is not a directory.\n") - os.Exit(1) - } - - // Find and test validity of output path. - if flag.NArg() > 1 { - c.Output = filepath.Clean(flag.Arg(1)) - - stat, err := os.Lstat(c.Output) - if err != nil { - if !os.IsNotExist(err) { - fmt.Fprintf(os.Stderr, "Output path: %v.\n", err) - os.Exit(1) - } - - // Create output directory - err = os.MkdirAll(c.Output, 0744) - if err != nil { - fmt.Fprintf(os.Stderr, "Create Output directory: %v.\n", err) - os.Exit(1) - } - - } else if !stat.IsDir() { - fmt.Fprintf(os.Stderr, "Output path is not a directory.\n") - os.Exit(1) - } - } else { - // If no output path is specified, use the current directory. - c.Output, err = os.Getwd() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to determine current working directory: %v\n", err) - os.Exit(1) - } - } - - // Process build tags. - if len(tagstr) > 0 { - c.Tags = strings.Split(tagstr, ",") - } else { - c.Tags = append(c.Tags, "debug") - } - - return c -} diff --git a/go-bindata/main.go b/go-bindata/main.go index 1ebfade..bd07afe 100644 --- a/go-bindata/main.go +++ b/go-bindata/main.go @@ -5,10 +5,116 @@ package main import ( + "flag" "fmt" + "github.com/jteeuwen/go-bindata" + "os" + "path/filepath" + "strings" ) func main() { - cfg := NewConfig() - fmt.Println(cfg) + cfg, status := parseArgs() + err := bindata.Translate(cfg, status) + + if err != nil { + fmt.Fprintf(os.Stderr, "bindata: %v", err) + } +} + +// parseArgs create s a new, filled configuration instance +// by reading and parsing command line options. +// +// This function exits the program with an error, if +// any of the command line options are incorrect. +func parseArgs() (*bindata.Config, bindata.ProgressFunc) { + var version, quiet bool + var tagstr string + + c := new(bindata.Config) + + flag.Usage = func() { + fmt.Printf("Usage: %s [options] []\n\n", os.Args[0]) + flag.PrintDefaults() + } + + flag.StringVar(&tagstr, "tags", "", "Comma-separated list of build tags to include.") + flag.StringVar(&c.Prefix, "prefix", "", "Optional path prefix to strip off map keys and function names.") + flag.BoolVar(&c.NoMemCopy, "nomemcopy", false, "Use a .rodata hack to get rid of unnecessary memcopies. Refer to the documentation to see what implications this carries.") + flag.BoolVar(&c.Compress, "compress", false, "Assets will be GZIP compressed when this flag is specified.") + flag.BoolVar(&version, "version", false, "Displays version information.") + flag.BoolVar(&quiet, "quiet", false, "Do not print conversion status.") + flag.Parse() + + if version { + fmt.Printf("%s\n", Version()) + os.Exit(0) + } + + // Make sure we have in/output paths. + if flag.NArg() == 0 { + fmt.Fprintf(os.Stderr, "Missing asset directory.\n") + os.Exit(1) + } + + // Test validity of input path. + c.Input = filepath.Clean(flag.Arg(0)) + + stat, err := os.Lstat(c.Input) + if err != nil { + fmt.Fprintf(os.Stderr, "Input path: %v.\n", err) + os.Exit(1) + } + + if !stat.IsDir() { + fmt.Fprintf(os.Stderr, "Input path is not a directory.\n") + os.Exit(1) + } + + // Find and test validity of output path. + if flag.NArg() > 1 { + c.Output = filepath.Clean(flag.Arg(1)) + + stat, err := os.Lstat(c.Output) + if err != nil { + if !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Output path: %v.\n", err) + os.Exit(1) + } + + // Create output directory + err = os.MkdirAll(c.Output, 0744) + if err != nil { + fmt.Fprintf(os.Stderr, "Create Output directory: %v.\n", err) + os.Exit(1) + } + + } else if !stat.IsDir() { + fmt.Fprintf(os.Stderr, "Output path is not a directory.\n") + os.Exit(1) + } + } else { + // If no output path is specified, use the current directory. + c.Output, err = os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to determine current working directory: %v\n", err) + os.Exit(1) + } + } + + // Process build tags. + if len(tagstr) > 0 { + c.Tags = strings.Split(tagstr, ",") + } else { + c.Tags = append(c.Tags, "debug") + } + + if quiet { + return c, nil + } + + return c, func(file string, current, total int) bool { + fmt.Printf("[%d/%d] %s\n", current, total, file) + return false + } } diff --git a/testdata/a/test.asset b/testdata/a/test.asset new file mode 100644 index 0000000..fb67027 --- /dev/null +++ b/testdata/a/test.asset @@ -0,0 +1 @@ +sample file diff --git a/testdata/b/test.asset b/testdata/b/test.asset new file mode 100644 index 0000000..ab2a2d9 --- /dev/null +++ b/testdata/b/test.asset @@ -0,0 +1 @@ +// sample file