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 }