2012-02-28 03:50:17 +00:00
/ *
Copyright 2012 Google Inc .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2013-06-10 22:55:31 +00:00
// The genfileembed command embeds resources into Go files, to eliminate run-time
// dependencies on files on the filesystem.
2012-02-28 03:50:17 +00:00
package main
import (
"bytes"
2012-11-28 23:54:56 +00:00
"compress/zlib"
2013-06-09 23:11:03 +00:00
"crypto/sha1"
"encoding/base64"
2012-02-28 03:50:17 +00:00
"flag"
"fmt"
"go/parser"
2012-11-05 17:43:20 +00:00
"go/printer"
2012-02-28 03:50:17 +00:00
"go/token"
2012-11-28 23:54:56 +00:00
"io"
2012-02-28 03:50:17 +00:00
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
2013-06-22 03:17:08 +00:00
"time"
2013-06-09 23:11:03 +00:00
"camlistore.org/pkg/rollsum"
)
var (
processAll = flag . Bool ( "all" , false , "process all files (if false, only process modified files)" )
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." )
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" )
2013-06-09 23:47:45 +00:00
chunkPackage = flag . String ( "chunk-package" , "" , "Package to hold chunks" )
2014-08-17 01:19:38 +00:00
destFilesStderr = flag . Bool ( "output-files-stderr" , false , "Write the absolute path of all output files to stderr prefixed with OUTPUT:" )
2014-10-17 11:11:23 +00:00
patternFilename = flag . String ( "pattern-file" , "fileembed.go" , "Filepath relative to <dir> from which to read the #fileembed pattern" )
2014-10-17 18:58:15 +00:00
buildTags = flag . String ( "build-tags" , "" , "Add these tags as +build constraints to the resulting zembed_*.go files" )
2012-02-28 03:50:17 +00:00
)
2012-11-28 23:54:56 +00:00
const (
maxUncompressed = 50 << 10 // 50KB
// Threshold ratio for compression.
// Files which don't compress at least as well are kept uncompressed.
zRatio = 0.5
)
2013-06-05 00:05:40 +00:00
func usage ( ) {
fmt . Fprintf ( os . Stderr , "usage: genfileembed [flags] [<dir>]\n" )
flag . PrintDefaults ( )
os . Exit ( 2 )
}
2012-02-28 03:50:17 +00:00
func main ( ) {
2013-06-05 00:05:40 +00:00
flag . Usage = usage
2012-02-28 03:50:17 +00:00
flag . Parse ( )
2013-06-05 00:05:40 +00:00
2014-08-17 01:19:38 +00:00
absPath , err := os . Getwd ( ) // absolute path of output directory
if err != nil {
log . Fatal ( err )
}
2012-02-28 03:50:17 +00:00
dir := "."
switch flag . NArg ( ) {
case 0 :
case 1 :
dir = flag . Arg ( 0 )
if err := os . Chdir ( dir ) ; err != nil {
log . Fatalf ( "chdir(%q) = %v" , dir , err )
}
2014-08-17 01:19:38 +00:00
if filepath . IsAbs ( dir ) {
absPath = dir
} else {
absPath = filepath . Join ( absPath , dir )
}
2012-02-28 03:50:17 +00:00
default :
2013-06-05 00:05:40 +00:00
flag . Usage ( )
2012-02-28 03:50:17 +00:00
}
2013-06-22 03:17:08 +00:00
pkgName , filePattern , fileEmbedModTime , err := parseFileEmbed ( )
2012-02-28 03:50:17 +00:00
if err != nil {
2014-10-17 11:11:23 +00:00
log . Fatalf ( "Error parsing %s/%s: %v" , dir , * patternFilename , err )
2012-02-28 03:50:17 +00:00
}
2012-02-29 06:49:52 +00:00
2012-02-28 03:50:17 +00:00
for _ , fileName := range matchingFiles ( filePattern ) {
fi , err := os . Stat ( fileName )
if err != nil {
log . Fatal ( err )
}
2013-06-22 03:17:08 +00:00
2014-04-12 05:00:09 +00:00
embedName := "zembed_" + strings . Replace ( fileName , string ( filepath . Separator ) , "_" , - 1 ) + ".go"
2014-08-17 01:19:38 +00:00
if * destFilesStderr {
fmt . Fprintf ( os . Stderr , "OUTPUT:%s\n" , filepath . Join ( absPath , embedName ) )
}
2013-06-22 03:17:08 +00:00
zfi , zerr := os . Stat ( embedName )
genFile := func ( ) bool {
if * processAll || zerr != nil {
return true
}
if zfi . ModTime ( ) . Before ( fi . ModTime ( ) ) {
return true
}
if zfi . ModTime ( ) . Before ( fileEmbedModTime ) {
return true
}
return false
}
if ! genFile ( ) {
2012-02-28 03:50:17 +00:00
continue
}
2014-04-06 02:34:13 +00:00
log . Printf ( "Updating %s (package %s)" , embedName , pkgName )
2012-11-28 23:54:56 +00:00
2013-05-24 21:19:29 +00:00
bs , err := ioutil . ReadFile ( fileName )
if err != nil {
log . Fatal ( err )
}
zb , fileSize := compressFile ( bytes . NewReader ( bs ) )
2012-11-28 23:54:56 +00:00
ratio := float64 ( len ( zb ) ) / float64 ( fileSize )
byteStreamType := ""
2013-06-09 23:11:03 +00:00
var qb [ ] byte // quoted string, or Go expression evaluating to a string
var imports string
if * chunkThreshold > 0 && int64 ( len ( bs ) ) > * chunkThreshold {
byteStreamType = "fileembed.Multi"
qb = chunksOf ( bs )
if * chunkPackage == "" {
log . Fatalf ( "Must provide a --chunk-package value with --chunk-threshold" )
}
imports = fmt . Sprintf ( "import chunkpkg \"%s\"\n" , * chunkPackage )
} else if fileSize < maxUncompressed || ratio > zRatio {
2012-11-28 23:54:56 +00:00
byteStreamType = "fileembed.String"
2013-06-09 23:11:03 +00:00
qb = quote ( bs )
2012-11-28 23:54:56 +00:00
} else {
2013-06-09 23:11:03 +00:00
byteStreamType = "fileembed.ZlibCompressedBase64"
qb = quote ( [ ] byte ( base64 . StdEncoding . EncodeToString ( zb ) ) )
2012-11-05 17:43:20 +00:00
}
2012-02-28 03:50:17 +00:00
var b bytes . Buffer
fmt . Fprintf ( & b , "// THIS FILE IS AUTO-GENERATED FROM %s\n" , fileName )
2014-10-17 18:58:15 +00:00
fmt . Fprintf ( & b , "// DO NOT EDIT.\n" )
if * buildTags != "" {
fmt . Fprintf ( & b , "// +build %s\n" , * buildTags )
}
fmt . Fprintf ( & b , "\n" )
2012-11-05 17:43:20 +00:00
fmt . Fprintf ( & b , "package %s\n\n" , pkgName )
fmt . Fprintf ( & b , "import \"time\"\n\n" )
2013-06-09 23:11:03 +00:00
fmt . Fprintf ( & b , "import \"" + * fileEmbedPkgPath + "\"\n\n" )
b . WriteString ( imports )
fmt . Fprintf ( & b , "func init() {\n\tFiles.Add(%q, %d, time.Unix(0, %d), %s(%s));\n}\n" ,
fileName , fileSize , fi . ModTime ( ) . UnixNano ( ) , byteStreamType , qb )
2012-11-05 17:43:20 +00:00
// gofmt it
fset := token . NewFileSet ( )
ast , err := parser . ParseFile ( fset , "" , b . Bytes ( ) , parser . ParseComments )
if err != nil {
log . Fatal ( err )
}
var clean bytes . Buffer
config := & printer . Config {
Mode : printer . TabIndent | printer . UseSpaces ,
Tabwidth : 8 ,
}
err = config . Fprint ( & clean , fset , ast )
if err != nil {
log . Fatal ( err )
}
2013-06-09 23:11:03 +00:00
if err := writeFileIfDifferent ( embedName , clean . Bytes ( ) ) ; err != nil {
2012-02-28 03:50:17 +00:00
log . Fatal ( err )
}
}
}
2013-06-09 23:11:03 +00:00
func writeFileIfDifferent ( filename string , contents [ ] byte ) error {
fi , err := os . Stat ( filename )
if err == nil && fi . Size ( ) == int64 ( len ( contents ) ) && contentsEqual ( filename , contents ) {
2014-02-28 05:00:01 +00:00
os . Chtimes ( filename , time . Now ( ) , time . Now ( ) )
2013-06-09 23:11:03 +00:00
return nil
}
return ioutil . WriteFile ( filename , contents , 0644 )
}
func contentsEqual ( filename string , contents [ ] byte ) bool {
got , err := ioutil . ReadFile ( filename )
if err != nil {
return false
}
return bytes . Equal ( got , contents )
}
2013-05-24 21:19:29 +00:00
func compressFile ( r io . Reader ) ( [ ] byte , int64 ) {
2012-11-28 23:54:56 +00:00
var zb bytes . Buffer
w := zlib . NewWriter ( & zb )
2013-05-24 21:19:29 +00:00
n , err := io . Copy ( w , r )
2012-11-28 23:54:56 +00:00
if err != nil {
log . Fatal ( err )
}
w . Close ( )
return zb . Bytes ( ) , n
}
func quote ( bs [ ] byte ) [ ] byte {
var qb bytes . Buffer
2014-06-20 17:09:52 +00:00
qb . WriteString ( ` fileembed.JoinStrings(" ` )
2012-11-28 23:54:56 +00:00
run := 0
2014-06-20 17:09:52 +00:00
concatCount := 0
2012-11-28 23:54:56 +00:00
for _ , b := range bs {
if b == '\n' {
qb . WriteString ( ` \n ` )
}
if b == '\n' || run > 80 {
2014-06-20 17:09:52 +00:00
// Prevent too many strings from being concatenated together.
// See https://code.google.com/p/go/issues/detail?id=8240
concatCount ++
if concatCount < 50 {
qb . WriteString ( "\" +\n\t\"" )
} else {
concatCount = 0
qb . WriteString ( "\",\n\t\"" )
}
2012-11-28 23:54:56 +00:00
run = 0
}
if b == '\n' {
continue
}
run ++
if b == '\\' {
qb . WriteString ( ` \\ ` )
continue
}
if b == '"' {
qb . WriteString ( ` \" ` )
continue
}
if ( b >= 32 && b <= 126 ) || b == '\t' {
qb . WriteByte ( b )
continue
}
fmt . Fprintf ( & qb , "\\x%02x" , b )
}
2014-06-20 17:09:52 +00:00
qb . WriteString ( ` ") ` )
2012-11-28 23:54:56 +00:00
return qb . Bytes ( )
}
2014-04-12 05:00:09 +00:00
// matchingFiles finds all files matching a regex that should be embedded. This
// skips files prefixed with "zembed_", since those are an implementation
// detail of the embedding process itself.
2012-02-28 03:50:17 +00:00
func matchingFiles ( p * regexp . Regexp ) [ ] string {
var f [ ] string
2014-04-12 05:00:09 +00:00
err := filepath . Walk ( "." , func ( path string , fi os . FileInfo , err error ) error {
2014-04-05 05:59:43 +00:00
if err != nil {
return err
2012-02-28 03:50:17 +00:00
}
2014-04-05 05:59:43 +00:00
n := filepath . Base ( path )
2014-04-12 05:00:09 +00:00
if ! fi . IsDir ( ) && ! strings . HasPrefix ( n , "zembed_" ) && p . MatchString ( n ) {
2014-04-05 05:59:43 +00:00
f = append ( f , path )
2012-02-28 03:50:17 +00:00
}
2014-04-05 05:59:43 +00:00
return nil
} )
2014-04-12 05:00:09 +00:00
if err != nil {
log . Fatalf ( "Error walking directory tree: %s" , err )
return nil
}
2012-02-28 03:50:17 +00:00
return f
}
2013-06-22 03:17:08 +00:00
func parseFileEmbed ( ) ( pkgName string , filePattern * regexp . Regexp , modTime time . Time , err error ) {
2014-10-17 11:11:23 +00:00
fe , err := os . Open ( * patternFilename )
2012-02-28 03:50:17 +00:00
if err != nil {
return
}
defer fe . Close ( )
2013-06-22 03:17:08 +00:00
fi , err := fe . Stat ( )
if err != nil {
return
}
modTime = fi . ModTime ( )
2012-02-28 03:50:17 +00:00
fs := token . NewFileSet ( )
2014-10-17 11:11:23 +00:00
astf , err := parser . ParseFile ( fs , * patternFilename , fe , parser . PackageClauseOnly | parser . ParseComments )
2012-02-28 03:50:17 +00:00
if err != nil {
return
}
pkgName = astf . Name . Name
if astf . Doc == nil {
err = fmt . Errorf ( "no package comment before the %q line" , "package " + pkgName )
return
}
pkgComment := astf . Doc . Text ( )
findPattern := regexp . MustCompile ( ` (?m)^#fileembed\s+pattern\s+(\S+)\s*$ ` )
m := findPattern . FindStringSubmatch ( pkgComment )
if m == nil {
err = fmt . Errorf ( "package comment lacks line of form: #fileembed pattern <pattern>" )
return
}
pattern := m [ 1 ]
filePattern , err = regexp . Compile ( pattern )
if err != nil {
err = fmt . Errorf ( "bad regexp %q: %v" , pattern , err )
return
}
return
}
2013-06-09 23:11:03 +00:00
// chunksOf takes a (presumably large) file's uncompressed input,
// rolling-checksum splits it into ~514 byte chunks, compresses each,
// base64s each, and writes chunk files out, with each file just
// defining an exported fileembed.Opener variable named C<xxxx> where
// xxxx is the first 8 lowercase hex digits of the SHA-1 of the chunk
// value pre-compression. The return value is a Go expression
// referencing each of those chunks concatenated together.
func chunksOf ( in [ ] byte ) ( stringExpression [ ] byte ) {
var multiParts [ ] [ ] byte
rs := rollsum . New ( )
const nBits = 9 // ~512 byte chunks
last := 0
for i , b := range in {
rs . Roll ( b )
2013-06-09 23:47:45 +00:00
if rs . OnSplitWithBits ( nBits ) || i == len ( in ) - 1 {
raw := in [ last : i + 1 ] // inclusive
2013-06-09 23:11:03 +00:00
last = i + 1
s1 := sha1 . New ( )
s1 . Write ( raw )
sha1hex := fmt . Sprintf ( "%x" , s1 . Sum ( nil ) ) [ : 8 ]
writeChunkFile ( sha1hex , raw )
multiParts = append ( multiParts , [ ] byte ( fmt . Sprintf ( "chunkpkg.C%s" , sha1hex ) ) )
}
}
return bytes . Join ( multiParts , [ ] byte ( ",\n\t" ) )
}
func writeChunkFile ( hex string , raw [ ] byte ) {
path := os . Getenv ( "GOPATH" )
if path == "" {
log . Fatalf ( "No GOPATH set" )
}
path = filepath . SplitList ( path ) [ 0 ]
2013-06-09 23:47:45 +00:00
file := filepath . Join ( path , "src" , filepath . FromSlash ( * chunkPackage ) , "chunk_" + hex + ".go" )
2013-06-09 23:11:03 +00:00
zb , _ := compressFile ( bytes . NewReader ( raw ) )
var buf bytes . Buffer
buf . WriteString ( "// THIS FILE IS AUTO-GENERATED. SEE README.\n\n" )
buf . WriteString ( "package chunkpkg\n" )
2013-06-09 23:47:45 +00:00
buf . WriteString ( "import \"" + * fileEmbedPkgPath + "\"\n\n" )
2013-06-09 23:11:03 +00:00
fmt . Fprintf ( & buf , "var C%s fileembed.Opener\n\nfunc init() { C%s = fileembed.ZlibCompressedBase64(%s)\n }\n" ,
hex ,
hex ,
quote ( [ ] byte ( base64 . StdEncoding . EncodeToString ( zb ) ) ) )
err := writeFileIfDifferent ( file , buf . Bytes ( ) )
if err != nil {
log . Fatalf ( "Error writing chunk %s to %v: %v" , hex , file , err )
}
}