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 }