github.com/liquid-dev/text@v0.3.3-liquid/internal/gen/gen.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package gen contains common code for the various code generation tools in the
     6  // text repository. Its usage ensures consistency between tools.
     7  //
     8  // This package defines command line flags that are common to most generation
     9  // tools. The flags allow for specifying specific Unicode and CLDR versions
    10  // in the public Unicode data repository (https://www.unicode.org/Public).
    11  //
    12  // A local Unicode data mirror can be set through the flag -local or the
    13  // environment variable UNICODE_DIR. The former takes precedence. The local
    14  // directory should follow the same structure as the public repository.
    15  //
    16  // IANA data can also optionally be mirrored by putting it in the iana directory
    17  // rooted at the top of the local mirror. Beware, though, that IANA data is not
    18  // versioned. So it is up to the developer to use the right version.
    19  package gen // import "github.com/liquid-dev/text/internal/gen"
    20  
    21  import (
    22  	"bytes"
    23  	"flag"
    24  	"fmt"
    25  	"go/build"
    26  	"go/format"
    27  	"io"
    28  	"io/ioutil"
    29  	"log"
    30  	"net/http"
    31  	"os"
    32  	"path"
    33  	"path/filepath"
    34  	"regexp"
    35  	"strings"
    36  	"sync"
    37  	"unicode"
    38  
    39  	"github.com/liquid-dev/text/unicode/cldr"
    40  )
    41  
    42  var (
    43  	url = flag.String("url",
    44  		"https://www.unicode.org/Public",
    45  		"URL of Unicode database directory")
    46  	iana = flag.String("iana",
    47  		"http://www.iana.org",
    48  		"URL of the IANA repository")
    49  	unicodeVersion = flag.String("unicode",
    50  		getEnv("UNICODE_VERSION", unicode.Version),
    51  		"unicode version to use")
    52  	cldrVersion = flag.String("cldr",
    53  		getEnv("CLDR_VERSION", cldr.Version),
    54  		"cldr version to use")
    55  )
    56  
    57  func getEnv(name, def string) string {
    58  	if v := os.Getenv(name); v != "" {
    59  		return v
    60  	}
    61  	return def
    62  }
    63  
    64  // Init performs common initialization for a gen command. It parses the flags
    65  // and sets up the standard logging parameters.
    66  func Init() {
    67  	log.SetPrefix("")
    68  	log.SetFlags(log.Lshortfile)
    69  	flag.Parse()
    70  }
    71  
    72  const header = `// Code generated by running "go generate" in github.com/liquid-dev/text. DO NOT EDIT.
    73  
    74  `
    75  
    76  // UnicodeVersion reports the requested Unicode version.
    77  func UnicodeVersion() string {
    78  	return *unicodeVersion
    79  }
    80  
    81  // CLDRVersion reports the requested CLDR version.
    82  func CLDRVersion() string {
    83  	return *cldrVersion
    84  }
    85  
    86  var tags = []struct{ version, buildTags string }{
    87  	{"9.0.0", "!go1.10"},
    88  	{"10.0.0", "go1.10,!go1.13"},
    89  	{"11.0.0", "go1.13,!go1.14"},
    90  	{"12.0.0", "go1.14"},
    91  }
    92  
    93  // buildTags reports the build tags used for the current Unicode version.
    94  func buildTags() string {
    95  	v := UnicodeVersion()
    96  	for _, e := range tags {
    97  		if e.version == v {
    98  			return e.buildTags
    99  		}
   100  	}
   101  	log.Fatalf("Unknown build tags for Unicode version %q.", v)
   102  	return ""
   103  }
   104  
   105  // IsLocal reports whether data files are available locally.
   106  func IsLocal() bool {
   107  	dir, err := localReadmeFile()
   108  	if err != nil {
   109  		return false
   110  	}
   111  	if _, err = os.Stat(dir); err != nil {
   112  		return false
   113  	}
   114  	return true
   115  }
   116  
   117  // OpenUCDFile opens the requested UCD file. The file is specified relative to
   118  // the public Unicode root directory. It will call log.Fatal if there are any
   119  // errors.
   120  func OpenUCDFile(file string) io.ReadCloser {
   121  	return openUnicode(path.Join(*unicodeVersion, "ucd", file))
   122  }
   123  
   124  // OpenCLDRCoreZip opens the CLDR core zip file. It will call log.Fatal if there
   125  // are any errors.
   126  func OpenCLDRCoreZip() io.ReadCloser {
   127  	return OpenUnicodeFile("cldr", *cldrVersion, "core.zip")
   128  }
   129  
   130  // OpenUnicodeFile opens the requested file of the requested category from the
   131  // root of the Unicode data archive. The file is specified relative to the
   132  // public Unicode root directory. If version is "", it will use the default
   133  // Unicode version. It will call log.Fatal if there are any errors.
   134  func OpenUnicodeFile(category, version, file string) io.ReadCloser {
   135  	if version == "" {
   136  		version = UnicodeVersion()
   137  	}
   138  	return openUnicode(path.Join(category, version, file))
   139  }
   140  
   141  // OpenIANAFile opens the requested IANA file. The file is specified relative
   142  // to the IANA root, which is typically either http://www.iana.org or the
   143  // iana directory in the local mirror. It will call log.Fatal if there are any
   144  // errors.
   145  func OpenIANAFile(path string) io.ReadCloser {
   146  	return Open(*iana, "iana", path)
   147  }
   148  
   149  var (
   150  	dirMutex sync.Mutex
   151  	localDir string
   152  )
   153  
   154  const permissions = 0755
   155  
   156  func localReadmeFile() (string, error) {
   157  	p, err := build.Import("github.com/liquid-dev/text", "", build.FindOnly)
   158  	if err != nil {
   159  		return "", fmt.Errorf("Could not locate package: %v", err)
   160  	}
   161  	return filepath.Join(p.Dir, "DATA", "README"), nil
   162  }
   163  
   164  func getLocalDir() string {
   165  	dirMutex.Lock()
   166  	defer dirMutex.Unlock()
   167  
   168  	readme, err := localReadmeFile()
   169  	if err != nil {
   170  		log.Fatal(err)
   171  	}
   172  	dir := filepath.Dir(readme)
   173  	if _, err := os.Stat(readme); err != nil {
   174  		if err := os.MkdirAll(dir, permissions); err != nil {
   175  			log.Fatalf("Could not create directory: %v", err)
   176  		}
   177  		ioutil.WriteFile(readme, []byte(readmeTxt), permissions)
   178  	}
   179  	return dir
   180  }
   181  
   182  const readmeTxt = `Generated by github.com/liquid-dev/text/internal/gen. DO NOT EDIT.
   183  
   184  This directory contains downloaded files used to generate the various tables
   185  in the github.com/liquid-dev/text subrepo.
   186  
   187  Note that the language subtag repo (iana/assignments/language-subtag-registry)
   188  and all other times in the iana subdirectory are not versioned and will need
   189  to be periodically manually updated. The easiest way to do this is to remove
   190  the entire iana directory. This is mostly of concern when updating the language
   191  package.
   192  `
   193  
   194  // Open opens subdir/path if a local directory is specified and the file exists,
   195  // where subdir is a directory relative to the local root, or fetches it from
   196  // urlRoot/path otherwise. It will call log.Fatal if there are any errors.
   197  func Open(urlRoot, subdir, path string) io.ReadCloser {
   198  	file := filepath.Join(getLocalDir(), subdir, filepath.FromSlash(path))
   199  	return open(file, urlRoot, path)
   200  }
   201  
   202  func openUnicode(path string) io.ReadCloser {
   203  	file := filepath.Join(getLocalDir(), filepath.FromSlash(path))
   204  	return open(file, *url, path)
   205  }
   206  
   207  // TODO: automatically periodically update non-versioned files.
   208  
   209  func open(file, urlRoot, path string) io.ReadCloser {
   210  	if f, err := os.Open(file); err == nil {
   211  		return f
   212  	}
   213  	r := get(urlRoot, path)
   214  	defer r.Close()
   215  	b, err := ioutil.ReadAll(r)
   216  	if err != nil {
   217  		log.Fatalf("Could not download file: %v", err)
   218  	}
   219  	os.MkdirAll(filepath.Dir(file), permissions)
   220  	if err := ioutil.WriteFile(file, b, permissions); err != nil {
   221  		log.Fatalf("Could not create file: %v", err)
   222  	}
   223  	return ioutil.NopCloser(bytes.NewReader(b))
   224  }
   225  
   226  func get(root, path string) io.ReadCloser {
   227  	url := root + "/" + path
   228  	fmt.Printf("Fetching %s...", url)
   229  	defer fmt.Println(" done.")
   230  	resp, err := http.Get(url)
   231  	if err != nil {
   232  		log.Fatalf("HTTP GET: %v", err)
   233  	}
   234  	if resp.StatusCode != 200 {
   235  		log.Fatalf("Bad GET status for %q: %q", url, resp.Status)
   236  	}
   237  	return resp.Body
   238  }
   239  
   240  // TODO: use Write*Version in all applicable packages.
   241  
   242  // WriteUnicodeVersion writes a constant for the Unicode version from which the
   243  // tables are generated.
   244  func WriteUnicodeVersion(w io.Writer) {
   245  	fmt.Fprintf(w, "// UnicodeVersion is the Unicode version from which the tables in this package are derived.\n")
   246  	fmt.Fprintf(w, "const UnicodeVersion = %q\n\n", UnicodeVersion())
   247  }
   248  
   249  // WriteCLDRVersion writes a constant for the CLDR version from which the
   250  // tables are generated.
   251  func WriteCLDRVersion(w io.Writer) {
   252  	fmt.Fprintf(w, "// CLDRVersion is the CLDR version from which the tables in this package are derived.\n")
   253  	fmt.Fprintf(w, "const CLDRVersion = %q\n\n", CLDRVersion())
   254  }
   255  
   256  // WriteGoFile prepends a standard file comment and package statement to the
   257  // given bytes, applies gofmt, and writes them to a file with the given name.
   258  // It will call log.Fatal if there are any errors.
   259  func WriteGoFile(filename, pkg string, b []byte) {
   260  	w, err := os.Create(filename)
   261  	if err != nil {
   262  		log.Fatalf("Could not create file %s: %v", filename, err)
   263  	}
   264  	defer w.Close()
   265  	if _, err = WriteGo(w, pkg, "", b); err != nil {
   266  		log.Fatalf("Error writing file %s: %v", filename, err)
   267  	}
   268  }
   269  
   270  func fileToPattern(filename string) string {
   271  	suffix := ".go"
   272  	if strings.HasSuffix(filename, "_test.go") {
   273  		suffix = "_test.go"
   274  	}
   275  	prefix := filename[:len(filename)-len(suffix)]
   276  	return fmt.Sprint(prefix, "%s", suffix)
   277  }
   278  
   279  func updateBuildTags(pattern string) {
   280  	for _, t := range tags {
   281  		oldFile := fmt.Sprintf(pattern, t.version)
   282  		b, err := ioutil.ReadFile(oldFile)
   283  		if err != nil {
   284  			continue
   285  		}
   286  		build := fmt.Sprintf("// +build %s", t.buildTags)
   287  		b = regexp.MustCompile(`// \+build .*`).ReplaceAll(b, []byte(build))
   288  		err = ioutil.WriteFile(oldFile, b, 0644)
   289  		if err != nil {
   290  			log.Fatal(err)
   291  		}
   292  	}
   293  }
   294  
   295  // WriteVersionedGoFile prepends a standard file comment, adds build tags to
   296  // version the file for the current Unicode version, and package statement to
   297  // the given bytes, applies gofmt, and writes them to a file with the given
   298  // name. It will call log.Fatal if there are any errors.
   299  func WriteVersionedGoFile(filename, pkg string, b []byte) {
   300  	pattern := fileToPattern(filename)
   301  	updateBuildTags(pattern)
   302  	filename = fmt.Sprintf(pattern, UnicodeVersion())
   303  
   304  	w, err := os.Create(filename)
   305  	if err != nil {
   306  		log.Fatalf("Could not create file %s: %v", filename, err)
   307  	}
   308  	defer w.Close()
   309  	if _, err = WriteGo(w, pkg, buildTags(), b); err != nil {
   310  		log.Fatalf("Error writing file %s: %v", filename, err)
   311  	}
   312  }
   313  
   314  // WriteGo prepends a standard file comment and package statement to the given
   315  // bytes, applies gofmt, and writes them to w.
   316  func WriteGo(w io.Writer, pkg, tags string, b []byte) (n int, err error) {
   317  	src := []byte(header)
   318  	if tags != "" {
   319  		src = append(src, fmt.Sprintf("// +build %s\n\n", tags)...)
   320  	}
   321  	src = append(src, fmt.Sprintf("package %s\n\n", pkg)...)
   322  	src = append(src, b...)
   323  	formatted, err := format.Source(src)
   324  	if err != nil {
   325  		// Print the generated code even in case of an error so that the
   326  		// returned error can be meaningfully interpreted.
   327  		n, _ = w.Write(src)
   328  		return n, err
   329  	}
   330  	return w.Write(formatted)
   331  }
   332  
   333  // Repackage rewrites a Go file from belonging to package main to belonging to
   334  // the given package.
   335  func Repackage(inFile, outFile, pkg string) {
   336  	src, err := ioutil.ReadFile(inFile)
   337  	if err != nil {
   338  		log.Fatalf("reading %s: %v", inFile, err)
   339  	}
   340  	const toDelete = "package main\n\n"
   341  	i := bytes.Index(src, []byte(toDelete))
   342  	if i < 0 {
   343  		log.Fatalf("Could not find %q in %s.", toDelete, inFile)
   344  	}
   345  	w := &bytes.Buffer{}
   346  	w.Write(src[i+len(toDelete):])
   347  	WriteGoFile(outFile, pkg, w.Bytes())
   348  }