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  }