github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/fileembed/genfileembed/genfileembed.go (about)

     1  /*
     2  Copyright 2012 Google Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // The genfileembed command embeds resources into Go files, to eliminate run-time
    18  // dependencies on files on the filesystem.
    19  package main
    20  
    21  import (
    22  	"bytes"
    23  	"compress/zlib"
    24  	"crypto/sha1"
    25  	"encoding/base64"
    26  	"flag"
    27  	"fmt"
    28  	"go/parser"
    29  	"go/printer"
    30  	"go/token"
    31  	"io"
    32  	"io/ioutil"
    33  	"log"
    34  	"os"
    35  	"path/filepath"
    36  	"regexp"
    37  	"strings"
    38  	"time"
    39  
    40  	"camlistore.org/pkg/rollsum"
    41  )
    42  
    43  var (
    44  	processAll = flag.Bool("all", false, "process all files (if false, only process modified files)")
    45  
    46  	fileEmbedPkgPath = flag.String("fileembed-package", "camlistore.org/pkg/fileembed", "the Go package name for fileembed. If you have vendored fileembed (e.g. with goven), you can use this flag to ensure that generated code imports the vendored package.")
    47  
    48  	chunkThreshold = flag.Int64("chunk-threshold", 0, "If non-zero, the maximum size of a file before it's cut up into content-addressable chunks with a rolling checksum")
    49  	chunkPackage   = flag.String("chunk-package", "", "Package to hold chunks")
    50  )
    51  
    52  const (
    53  	maxUncompressed = 50 << 10 // 50KB
    54  	// Threshold ratio for compression.
    55  	// Files which don't compress at least as well are kept uncompressed.
    56  	zRatio = 0.5
    57  )
    58  
    59  func usage() {
    60  	fmt.Fprintf(os.Stderr, "usage: genfileembed [flags] [<dir>]\n")
    61  	flag.PrintDefaults()
    62  	os.Exit(2)
    63  }
    64  
    65  func main() {
    66  	flag.Usage = usage
    67  	flag.Parse()
    68  
    69  	dir := "."
    70  	switch flag.NArg() {
    71  	case 0:
    72  	case 1:
    73  		dir = flag.Arg(0)
    74  		if err := os.Chdir(dir); err != nil {
    75  			log.Fatalf("chdir(%q) = %v", dir, err)
    76  		}
    77  	default:
    78  		flag.Usage()
    79  	}
    80  
    81  	pkgName, filePattern, fileEmbedModTime, err := parseFileEmbed()
    82  	if err != nil {
    83  		log.Fatalf("Error parsing %s/fileembed.go: %v", dir, err)
    84  	}
    85  
    86  	for _, fileName := range matchingFiles(filePattern) {
    87  		fi, err := os.Stat(fileName)
    88  		if err != nil {
    89  			log.Fatal(err)
    90  		}
    91  
    92  		embedName := "zembed_" + fileName + ".go"
    93  		zfi, zerr := os.Stat(embedName)
    94  		genFile := func() bool {
    95  			if *processAll || zerr != nil {
    96  				return true
    97  			}
    98  			if zfi.ModTime().Before(fi.ModTime()) {
    99  				return true
   100  			}
   101  			if zfi.ModTime().Before(fileEmbedModTime) {
   102  				return true
   103  			}
   104  			return false
   105  		}
   106  		if !genFile() {
   107  			continue
   108  		}
   109  		log.Printf("Updating %s (package %s)", filepath.Join(dir, embedName), pkgName)
   110  
   111  		bs, err := ioutil.ReadFile(fileName)
   112  		if err != nil {
   113  			log.Fatal(err)
   114  		}
   115  
   116  		zb, fileSize := compressFile(bytes.NewReader(bs))
   117  		ratio := float64(len(zb)) / float64(fileSize)
   118  		byteStreamType := ""
   119  		var qb []byte // quoted string, or Go expression evaluating to a string
   120  		var imports string
   121  		if *chunkThreshold > 0 && int64(len(bs)) > *chunkThreshold {
   122  			byteStreamType = "fileembed.Multi"
   123  			qb = chunksOf(bs)
   124  			if *chunkPackage == "" {
   125  				log.Fatalf("Must provide a --chunk-package value with --chunk-threshold")
   126  			}
   127  			imports = fmt.Sprintf("import chunkpkg \"%s\"\n", *chunkPackage)
   128  		} else if fileSize < maxUncompressed || ratio > zRatio {
   129  			byteStreamType = "fileembed.String"
   130  			qb = quote(bs)
   131  		} else {
   132  			byteStreamType = "fileembed.ZlibCompressedBase64"
   133  			qb = quote([]byte(base64.StdEncoding.EncodeToString(zb)))
   134  		}
   135  
   136  		var b bytes.Buffer
   137  		fmt.Fprintf(&b, "// THIS FILE IS AUTO-GENERATED FROM %s\n", fileName)
   138  		fmt.Fprintf(&b, "// DO NOT EDIT.\n\n")
   139  		fmt.Fprintf(&b, "package %s\n\n", pkgName)
   140  		fmt.Fprintf(&b, "import \"time\"\n\n")
   141  		fmt.Fprintf(&b, "import \""+*fileEmbedPkgPath+"\"\n\n")
   142  		b.WriteString(imports)
   143  		fmt.Fprintf(&b, "func init() {\n\tFiles.Add(%q, %d, time.Unix(0, %d), %s(%s));\n}\n",
   144  			fileName, fileSize, fi.ModTime().UnixNano(), byteStreamType, qb)
   145  
   146  		// gofmt it
   147  		fset := token.NewFileSet()
   148  		ast, err := parser.ParseFile(fset, "", b.Bytes(), parser.ParseComments)
   149  		if err != nil {
   150  			log.Fatal(err)
   151  		}
   152  
   153  		var clean bytes.Buffer
   154  		config := &printer.Config{
   155  			Mode:     printer.TabIndent | printer.UseSpaces,
   156  			Tabwidth: 8,
   157  		}
   158  		err = config.Fprint(&clean, fset, ast)
   159  		if err != nil {
   160  			log.Fatal(err)
   161  		}
   162  
   163  		if err := writeFileIfDifferent(embedName, clean.Bytes()); err != nil {
   164  			log.Fatal(err)
   165  		}
   166  	}
   167  }
   168  
   169  func writeFileIfDifferent(filename string, contents []byte) error {
   170  	fi, err := os.Stat(filename)
   171  	if err == nil && fi.Size() == int64(len(contents)) && contentsEqual(filename, contents) {
   172  		return nil
   173  	}
   174  	return ioutil.WriteFile(filename, contents, 0644)
   175  }
   176  
   177  func contentsEqual(filename string, contents []byte) bool {
   178  	got, err := ioutil.ReadFile(filename)
   179  	if err != nil {
   180  		return false
   181  	}
   182  	return bytes.Equal(got, contents)
   183  }
   184  
   185  func compressFile(r io.Reader) ([]byte, int64) {
   186  	var zb bytes.Buffer
   187  	w := zlib.NewWriter(&zb)
   188  	n, err := io.Copy(w, r)
   189  	if err != nil {
   190  		log.Fatal(err)
   191  	}
   192  	w.Close()
   193  	return zb.Bytes(), n
   194  }
   195  
   196  func quote(bs []byte) []byte {
   197  	var qb bytes.Buffer
   198  	qb.WriteByte('"')
   199  	run := 0
   200  	for _, b := range bs {
   201  		if b == '\n' {
   202  			qb.WriteString(`\n`)
   203  		}
   204  		if b == '\n' || run > 80 {
   205  			qb.WriteString("\" +\n\t\"")
   206  			run = 0
   207  		}
   208  		if b == '\n' {
   209  			continue
   210  		}
   211  		run++
   212  		if b == '\\' {
   213  			qb.WriteString(`\\`)
   214  			continue
   215  		}
   216  		if b == '"' {
   217  			qb.WriteString(`\"`)
   218  			continue
   219  		}
   220  		if (b >= 32 && b <= 126) || b == '\t' {
   221  			qb.WriteByte(b)
   222  			continue
   223  		}
   224  		fmt.Fprintf(&qb, "\\x%02x", b)
   225  	}
   226  	qb.WriteByte('"')
   227  	return qb.Bytes()
   228  }
   229  
   230  func matchingFiles(p *regexp.Regexp) []string {
   231  	var f []string
   232  	d, err := os.Open(".")
   233  	if err != nil {
   234  		log.Fatal(err)
   235  	}
   236  	defer d.Close()
   237  	names, err := d.Readdirnames(-1)
   238  	if err != nil {
   239  		log.Fatal(err)
   240  	}
   241  	for _, n := range names {
   242  		if strings.HasPrefix(n, "zembed_") {
   243  			continue
   244  		}
   245  		if p.MatchString(n) {
   246  			f = append(f, n)
   247  		}
   248  	}
   249  	return f
   250  }
   251  
   252  func parseFileEmbed() (pkgName string, filePattern *regexp.Regexp, modTime time.Time, err error) {
   253  	fe, err := os.Open("fileembed.go")
   254  	if err != nil {
   255  		return
   256  	}
   257  	defer fe.Close()
   258  
   259  	fi, err := fe.Stat()
   260  	if err != nil {
   261  		return
   262  	}
   263  	modTime = fi.ModTime()
   264  
   265  	fs := token.NewFileSet()
   266  	astf, err := parser.ParseFile(fs, "fileembed.go", fe, parser.PackageClauseOnly|parser.ParseComments)
   267  	if err != nil {
   268  		return
   269  	}
   270  	pkgName = astf.Name.Name
   271  
   272  	if astf.Doc == nil {
   273  		err = fmt.Errorf("no package comment before the %q line", "package "+pkgName)
   274  		return
   275  	}
   276  
   277  	pkgComment := astf.Doc.Text()
   278  	findPattern := regexp.MustCompile(`(?m)^#fileembed\s+pattern\s+(\S+)\s*$`)
   279  	m := findPattern.FindStringSubmatch(pkgComment)
   280  	if m == nil {
   281  		err = fmt.Errorf("package comment lacks line of form: #fileembed pattern <pattern>")
   282  		return
   283  	}
   284  	pattern := m[1]
   285  	filePattern, err = regexp.Compile(pattern)
   286  	if err != nil {
   287  		err = fmt.Errorf("bad regexp %q: %v", pattern, err)
   288  		return
   289  	}
   290  	return
   291  }
   292  
   293  // chunksOf takes a (presumably large) file's uncompressed input,
   294  // rolling-checksum splits it into ~514 byte chunks, compresses each,
   295  // base64s each, and writes chunk files out, with each file just
   296  // defining an exported fileembed.Opener variable named C<xxxx> where
   297  // xxxx is the first 8 lowercase hex digits of the SHA-1 of the chunk
   298  // value pre-compression.  The return value is a Go expression
   299  // referencing each of those chunks concatenated together.
   300  func chunksOf(in []byte) (stringExpression []byte) {
   301  	var multiParts [][]byte
   302  	rs := rollsum.New()
   303  	const nBits = 9 // ~512 byte chunks
   304  	last := 0
   305  	for i, b := range in {
   306  		rs.Roll(b)
   307  		if rs.OnSplitWithBits(nBits) || i == len(in)-1 {
   308  			raw := in[last : i+1] // inclusive
   309  			last = i + 1
   310  			s1 := sha1.New()
   311  			s1.Write(raw)
   312  			sha1hex := fmt.Sprintf("%x", s1.Sum(nil))[:8]
   313  			writeChunkFile(sha1hex, raw)
   314  			multiParts = append(multiParts, []byte(fmt.Sprintf("chunkpkg.C%s", sha1hex)))
   315  		}
   316  	}
   317  	return bytes.Join(multiParts, []byte(",\n\t"))
   318  }
   319  
   320  func writeChunkFile(hex string, raw []byte) {
   321  	path := os.Getenv("GOPATH")
   322  	if path == "" {
   323  		log.Fatalf("No GOPATH set")
   324  	}
   325  	path = filepath.SplitList(path)[0]
   326  	file := filepath.Join(path, "src", filepath.FromSlash(*chunkPackage), "chunk_"+hex+".go")
   327  	zb, _ := compressFile(bytes.NewReader(raw))
   328  	var buf bytes.Buffer
   329  	buf.WriteString("// THIS FILE IS AUTO-GENERATED. SEE README.\n\n")
   330  	buf.WriteString("package chunkpkg\n")
   331  	buf.WriteString("import \"" + *fileEmbedPkgPath + "\"\n\n")
   332  	fmt.Fprintf(&buf, "var C%s fileembed.Opener\n\nfunc init() { C%s = fileembed.ZlibCompressedBase64(%s)\n }\n",
   333  		hex,
   334  		hex,
   335  		quote([]byte(base64.StdEncoding.EncodeToString(zb))))
   336  	err := writeFileIfDifferent(file, buf.Bytes())
   337  	if err != nil {
   338  		log.Fatalf("Error writing chunk %s to %v: %v", hex, file, err)
   339  	}
   340  }