go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tools/cmd/assets/main.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package main hosts the utility that converts binary assets into assets.gen.go 16 // file, so that they can be baked directly into the executable. Intended to 17 // be used only for small files, like HTML templates. 18 // 19 // This utility is used via `go generate`. Corresponding incantation: 20 // 21 // //go:generate assets 22 package main 23 24 import ( 25 "bytes" 26 "crypto/sha256" 27 "flag" 28 "fmt" 29 "go/build" 30 "os" 31 "os/exec" 32 "path/filepath" 33 "sort" 34 "strings" 35 "text/template" 36 "time" 37 38 "go.chromium.org/luci/common/data/stringset" 39 "go.chromium.org/luci/common/flag/fixflagpos" 40 "go.chromium.org/luci/common/flag/stringlistflag" 41 ) 42 43 // defaultExts lists glob patterns for files to put into generated 44 // *.go file. 45 var defaultExts = stringset.NewFromSlice( 46 "*.css", 47 "*.html", 48 "*.js", 49 "*.tmpl", 50 ) 51 52 // funcMap contains functions used when rendering assets.gen.go template. 53 var funcMap = template.FuncMap{ 54 "asByteArray": asByteArray, 55 } 56 57 // assetsGenGoTmpl is template for generated assets.gen.go file. Result of 58 // the execution will also be passed through gofmt. 59 var assetsGenGoTmpl = template.Must(template.New("tmpl").Funcs(funcMap).Parse(strings.TrimSpace(` 60 // Copyright {{.Year}} The LUCI Authors. 61 // 62 // Licensed under the Apache License, Version 2.0 (the "License"); 63 // you may not use this file except in compliance with the License. 64 // You may obtain a copy of the License at 65 // 66 // http://www.apache.org/licenses/LICENSE-2.0 67 // 68 // Unless required by applicable law or agreed to in writing, software 69 // distributed under the License is distributed on an "AS IS" BASIS, 70 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 // See the License for the specific language governing permissions and 72 // limitations under the License. 73 74 // AUTOGENERATED. DO NOT EDIT. 75 76 // Package {{.PackageName}} is generated by go.chromium.org/luci/tools/cmd/assets. 77 // 78 // It contains all {{.Patterns}} files found in the package as byte arrays. 79 package {{.PackageName}} 80 81 // GetAsset returns an asset by its name. Returns nil if no such asset exists. 82 func GetAsset(name string) []byte { 83 return []byte(files[name]) 84 } 85 86 // GetAssetString is version of GetAsset that returns string instead of byte 87 // slice. Returns empty string if no such asset exists. 88 func GetAssetString(name string) string { 89 return files[name] 90 } 91 92 // GetAssetSHA256 returns the asset checksum. Returns nil if no such asset 93 // exists. 94 func GetAssetSHA256(name string) []byte { 95 data := fileSha256s[name] 96 if data == nil { 97 return nil 98 } 99 return append([]byte(nil), data...) 100 } 101 102 // Assets returns a map of all assets. 103 func Assets() map[string]string { 104 cpy := make(map[string]string, len(files)) 105 for k, v := range files { 106 cpy[k] = v 107 } 108 return cpy 109 } 110 111 var files = map[string]string{ 112 {{range .Assets}}{{.Path | printf "%q"}}: string({{.Body | asByteArray }}), 113 {{end}} 114 } 115 116 var fileSha256s = map[string][]byte{ 117 {{range .Assets}}{{.Path | printf "%q"}}: {{.SHA256 | asByteArray }}, 118 {{end}} 119 } 120 `))) 121 122 // assetsTestTmpl is template to assets_test.go file. 123 var assetsTestTmpl = template.Must(template.New("tmpl").Funcs(funcMap).Parse(strings.TrimSpace(` 124 // Copyright {{.Year}} The LUCI Authors. 125 // 126 // Licensed under the Apache License, Version 2.0 (the "License"); 127 // you may not use this file except in compliance with the License. 128 // You may obtain a copy of the License at 129 // 130 // http://www.apache.org/licenses/LICENSE-2.0 131 // 132 // Unless required by applicable law or agreed to in writing, software 133 // distributed under the License is distributed on an "AS IS" BASIS, 134 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 135 // See the License for the specific language governing permissions and 136 // limitations under the License. 137 138 // AUTOGENERATED. DO NOT EDIT. 139 140 // This file is generated by go.chromium.org/luci/tools/cmd/assets. 141 // 142 // It contains tests that ensure that assets embedded into the binary are 143 // identical to files on disk. 144 145 package {{.PackageName}} 146 147 import ( 148 "go/build" 149 "os" 150 "path/filepath" 151 "testing" 152 ) 153 154 func TestAssets(t *testing.T) { 155 t.Parallel() 156 157 pkg, err := build.ImportDir(".", build.FindOnly) 158 if err != nil { 159 t.Fatalf("can't load package: %s", err) 160 } 161 162 fail := false 163 for name := range Assets() { 164 GetAsset(name) // for code coverage 165 path := filepath.Join(pkg.Dir, filepath.FromSlash(name)) 166 blob, err := os.ReadFile(path) 167 if err != nil { 168 t.Errorf("can't read file with assets %q (%s) - %s", name, path, err) 169 fail = true 170 } else if string(blob) != GetAssetString(name) { 171 t.Errorf("embedded asset %q is out of date", name) 172 fail = true 173 } 174 } 175 176 if fail { 177 t.Fatalf("run 'go generate' to update assets.gen.go") 178 } 179 } 180 `))) 181 182 // templateData is passed to tmpl when rendering it. 183 type templateData struct { 184 Year int 185 Patterns []string 186 PackageName string 187 Assets []asset 188 } 189 190 // asset is single file to be embedded into assets.gen.go. 191 type asset struct { 192 Path string // path relative to package directory 193 Body []byte // body of the file 194 } 195 196 func (a asset) SHA256() []byte { 197 h := sha256.Sum256(a.Body) 198 return h[:] 199 } 200 201 type assetMap map[string]asset 202 203 func main() { 204 destPkg := "" 205 flag.StringVar(&destPkg, "dest-pkg", "", 206 `Path to a package to write assets.gen.go to (default is the same as input dir). `+ 207 `If it's different from the input dir, no *_test.go will be written, since `+ 208 `it wouldn't know how to discover the original files.`) 209 210 exts := stringlistflag.Flag{} 211 flag.Var(&exts, "ext", fmt.Sprintf( 212 `(repeatable) Additional extensions to pack up. `+ 213 `Should be in the form of a glob (e.g. '*.foo'). `+ 214 `By default this recognizes %q.`, defaultExts.ToSlice())) 215 216 flag.CommandLine.Parse(fixflagpos.Fix(os.Args[1:])) 217 218 var dir string 219 switch len(flag.Args()) { 220 case 0: 221 dir = "." 222 case 1: 223 dir = flag.Args()[0] 224 default: 225 fmt.Fprintf(os.Stderr, "usage: assets [dir] [-ext .ext]+\n") 226 os.Exit(2) 227 } 228 229 if destPkg == "" { 230 destPkg = dir 231 } 232 233 if err := run(dir, destPkg, exts); err != nil { 234 fmt.Fprintf(os.Stderr, "%s\n", err) 235 os.Exit(1) 236 } 237 } 238 239 // run generates assets.gen.go file with all assets discovered in the directory. 240 func run(inDir, destPkg string, extraExts []string) error { 241 exts := defaultExts.Union(stringset.NewFromSlice(extraExts...)).ToSlice() 242 sort.Strings(exts) 243 244 assets, err := findAssets(inDir, exts) 245 if err != nil { 246 return fmt.Errorf("can't find assets in %s - %s", inDir, err) 247 } 248 249 pkg, err := build.ImportDir(destPkg, build.ImportComment) 250 if err != nil { 251 return fmt.Errorf("can't find destination package %q - %s", destPkg, err) 252 } 253 254 err = generate(assetsGenGoTmpl, pkg.Name, assets, exts, filepath.Join(pkg.Dir, "assets.gen.go")) 255 if err != nil { 256 return fmt.Errorf("can't generate assets.gen.go - %s", err) 257 } 258 259 if samePaths(inDir, pkg.Dir) { 260 err = generate(assetsTestTmpl, pkg.Name, assets, exts, filepath.Join(pkg.Dir, "assets_test.go")) 261 if err != nil { 262 return fmt.Errorf("can't generate assets_test.go - %s", err) 263 } 264 } 265 266 return nil 267 } 268 269 // samePaths is true if two paths are identical when converted to absolutes. 270 // 271 // Panics if some path can't be converted to absolute. 272 func samePaths(a, b string) bool { 273 var err error 274 if a, err = filepath.Abs(a); err != nil { 275 panic(err) 276 } 277 if b, err = filepath.Abs(b); err != nil { 278 panic(err) 279 } 280 return a == b 281 } 282 283 // findAssets recursively scans pkgDir for asset files. 284 func findAssets(pkgDir string, exts []string) (assetMap, error) { 285 assets := assetMap{} 286 287 err := filepath.Walk(pkgDir, func(path string, info os.FileInfo, err error) error { 288 if err != nil || info.IsDir() || !isAssetFile(path, exts) { 289 return err 290 } 291 rel, err := filepath.Rel(pkgDir, path) 292 if err != nil { 293 return err 294 } 295 blob, err := os.ReadFile(path) 296 if err != nil { 297 return err 298 } 299 assets[filepath.ToSlash(rel)] = asset{ 300 Path: filepath.ToSlash(rel), 301 Body: blob, 302 } 303 return nil 304 }) 305 306 if err != nil { 307 return nil, err 308 } 309 return assets, nil 310 } 311 312 // isAssetFile returns true if `path` base name matches some of 313 // `assetExts` glob. 314 func isAssetFile(path string, assetExts []string) (ok bool) { 315 base := filepath.Base(path) 316 for _, pattern := range assetExts { 317 if match, _ := filepath.Match(pattern, base); match { 318 return true 319 } 320 } 321 return false 322 } 323 324 // generate executes the template, runs output through gofmt and dumps it to disk. 325 func generate(t *template.Template, pkgName string, assets assetMap, assetExts []string, path string) error { 326 keys := make([]string, 0, len(assets)) 327 for k := range assets { 328 keys = append(keys, k) 329 } 330 sort.Strings(keys) 331 332 data := templateData{ 333 Year: time.Now().Year(), 334 Patterns: assetExts, 335 PackageName: pkgName, 336 } 337 for _, key := range keys { 338 data.Assets = append(data.Assets, assets[key]) 339 } 340 341 out := bytes.Buffer{} 342 if err := t.Execute(&out, data); err != nil { 343 return err 344 } 345 346 formatted, err := gofmt(out.Bytes()) 347 if err != nil { 348 return fmt.Errorf("can't gofmt %s - %s", path, err) 349 } 350 351 return os.WriteFile(path, formatted, 0666) 352 } 353 354 // gofmt applies "gofmt -s" to the content of the buffer. 355 func gofmt(blob []byte) ([]byte, error) { 356 out := bytes.Buffer{} 357 cmd := exec.Command("gofmt", "-s") 358 cmd.Stdin = bytes.NewReader(blob) 359 cmd.Stdout = &out 360 cmd.Stderr = os.Stderr 361 if err := cmd.Run(); err != nil { 362 return nil, err 363 } 364 return out.Bytes(), nil 365 } 366 367 func asByteArray(blob []byte) string { 368 out := &bytes.Buffer{} 369 fmt.Fprintf(out, "[]byte{") 370 for i := 0; i < len(blob); i++ { 371 fmt.Fprintf(out, "%d, ", blob[i]) 372 if i%14 == 1 { 373 fmt.Fprintln(out) 374 } 375 } 376 fmt.Fprintf(out, "}") 377 return out.String() 378 }