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 (http://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.
19package gen // import "golang.org/x/text/internal/gen"
20
21import (
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	"sync"
35	"unicode"
36
37	"golang.org/x/text/unicode/cldr"
38)
39
40var (
41	url = flag.String("url",
42		"http://www.unicode.org/Public",
43		"URL of Unicode database directory")
44	iana = flag.String("iana",
45		"http://www.iana.org",
46		"URL of the IANA repository")
47	unicodeVersion = flag.String("unicode",
48		getEnv("UNICODE_VERSION", unicode.Version),
49		"unicode version to use")
50	cldrVersion = flag.String("cldr",
51		getEnv("CLDR_VERSION", cldr.Version),
52		"cldr version to use")
53)
54
55func getEnv(name, def string) string {
56	if v := os.Getenv(name); v != "" {
57		return v
58	}
59	return def
60}
61
62// Init performs common initialization for a gen command. It parses the flags
63// and sets up the standard logging parameters.
64func Init() {
65	log.SetPrefix("")
66	log.SetFlags(log.Lshortfile)
67	flag.Parse()
68}
69
70const header = `// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
71
72package %s
73
74`
75
76// UnicodeVersion reports the requested Unicode version.
77func UnicodeVersion() string {
78	return *unicodeVersion
79}
80
81// UnicodeVersion reports the requested CLDR version.
82func CLDRVersion() string {
83	return *cldrVersion
84}
85
86// IsLocal reports whether data files are available locally.
87func IsLocal() bool {
88	dir, err := localReadmeFile()
89	if err != nil {
90		return false
91	}
92	if _, err = os.Stat(dir); err != nil {
93		return false
94	}
95	return true
96}
97
98// OpenUCDFile opens the requested UCD file. The file is specified relative to
99// the public Unicode root directory. It will call log.Fatal if there are any
100// errors.
101func OpenUCDFile(file string) io.ReadCloser {
102	return openUnicode(path.Join(*unicodeVersion, "ucd", file))
103}
104
105// OpenCLDRCoreZip opens the CLDR core zip file. It will call log.Fatal if there
106// are any errors.
107func OpenCLDRCoreZip() io.ReadCloser {
108	return OpenUnicodeFile("cldr", *cldrVersion, "core.zip")
109}
110
111// OpenUnicodeFile opens the requested file of the requested category from the
112// root of the Unicode data archive. The file is specified relative to the
113// public Unicode root directory. If version is "", it will use the default
114// Unicode version. It will call log.Fatal if there are any errors.
115func OpenUnicodeFile(category, version, file string) io.ReadCloser {
116	if version == "" {
117		version = UnicodeVersion()
118	}
119	return openUnicode(path.Join(category, version, file))
120}
121
122// OpenIANAFile opens the requested IANA file. The file is specified relative
123// to the IANA root, which is typically either http://www.iana.org or the
124// iana directory in the local mirror. It will call log.Fatal if there are any
125// errors.
126func OpenIANAFile(path string) io.ReadCloser {
127	return Open(*iana, "iana", path)
128}
129
130var (
131	dirMutex sync.Mutex
132	localDir string
133)
134
135const permissions = 0755
136
137func localReadmeFile() (string, error) {
138	p, err := build.Import("golang.org/x/text", "", build.FindOnly)
139	if err != nil {
140		return "", fmt.Errorf("Could not locate package: %v", err)
141	}
142	return filepath.Join(p.Dir, "DATA", "README"), nil
143}
144
145func getLocalDir() string {
146	dirMutex.Lock()
147	defer dirMutex.Unlock()
148
149	readme, err := localReadmeFile()
150	if err != nil {
151		log.Fatal(err)
152	}
153	dir := filepath.Dir(readme)
154	if _, err := os.Stat(readme); err != nil {
155		if err := os.MkdirAll(dir, permissions); err != nil {
156			log.Fatalf("Could not create directory: %v", err)
157		}
158		ioutil.WriteFile(readme, []byte(readmeTxt), permissions)
159	}
160	return dir
161}
162
163const readmeTxt = `Generated by golang.org/x/text/internal/gen. DO NOT EDIT.
164
165This directory contains downloaded files used to generate the various tables
166in the golang.org/x/text subrepo.
167
168Note that the language subtag repo (iana/assignments/language-subtag-registry)
169and all other times in the iana subdirectory are not versioned and will need
170to be periodically manually updated. The easiest way to do this is to remove
171the entire iana directory. This is mostly of concern when updating the language
172package.
173`
174
175// Open opens subdir/path if a local directory is specified and the file exists,
176// where subdir is a directory relative to the local root, or fetches it from
177// urlRoot/path otherwise. It will call log.Fatal if there are any errors.
178func Open(urlRoot, subdir, path string) io.ReadCloser {
179	file := filepath.Join(getLocalDir(), subdir, filepath.FromSlash(path))
180	return open(file, urlRoot, path)
181}
182
183func openUnicode(path string) io.ReadCloser {
184	file := filepath.Join(getLocalDir(), filepath.FromSlash(path))
185	return open(file, *url, path)
186}
187
188// TODO: automatically periodically update non-versioned files.
189
190func open(file, urlRoot, path string) io.ReadCloser {
191	if f, err := os.Open(file); err == nil {
192		return f
193	}
194	r := get(urlRoot, path)
195	defer r.Close()
196	b, err := ioutil.ReadAll(r)
197	if err != nil {
198		log.Fatalf("Could not download file: %v", err)
199	}
200	os.MkdirAll(filepath.Dir(file), permissions)
201	if err := ioutil.WriteFile(file, b, permissions); err != nil {
202		log.Fatalf("Could not create file: %v", err)
203	}
204	return ioutil.NopCloser(bytes.NewReader(b))
205}
206
207func get(root, path string) io.ReadCloser {
208	url := root + "/" + path
209	fmt.Printf("Fetching %s...", url)
210	defer fmt.Println(" done.")
211	resp, err := http.Get(url)
212	if err != nil {
213		log.Fatalf("HTTP GET: %v", err)
214	}
215	if resp.StatusCode != 200 {
216		log.Fatalf("Bad GET status for %q: %q", url, resp.Status)
217	}
218	return resp.Body
219}
220
221// TODO: use Write*Version in all applicable packages.
222
223// WriteUnicodeVersion writes a constant for the Unicode version from which the
224// tables are generated.
225func WriteUnicodeVersion(w io.Writer) {
226	fmt.Fprintf(w, "// UnicodeVersion is the Unicode version from which the tables in this package are derived.\n")
227	fmt.Fprintf(w, "const UnicodeVersion = %q\n\n", UnicodeVersion())
228}
229
230// WriteCLDRVersion writes a constant for the CLDR version from which the
231// tables are generated.
232func WriteCLDRVersion(w io.Writer) {
233	fmt.Fprintf(w, "// CLDRVersion is the CLDR version from which the tables in this package are derived.\n")
234	fmt.Fprintf(w, "const CLDRVersion = %q\n\n", CLDRVersion())
235}
236
237// WriteGoFile prepends a standard file comment and package statement to the
238// given bytes, applies gofmt, and writes them to a file with the given name.
239// It will call log.Fatal if there are any errors.
240func WriteGoFile(filename, pkg string, b []byte) {
241	w, err := os.Create(filename)
242	if err != nil {
243		log.Fatalf("Could not create file %s: %v", filename, err)
244	}
245	defer w.Close()
246	if _, err = WriteGo(w, pkg, b); err != nil {
247		log.Fatalf("Error writing file %s: %v", filename, err)
248	}
249}
250
251// WriteGo prepends a standard file comment and package statement to the given
252// bytes, applies gofmt, and writes them to w.
253func WriteGo(w io.Writer, pkg string, b []byte) (n int, err error) {
254	src := []byte(fmt.Sprintf(header, pkg))
255	src = append(src, b...)
256	formatted, err := format.Source(src)
257	if err != nil {
258		// Print the generated code even in case of an error so that the
259		// returned error can be meaningfully interpreted.
260		n, _ = w.Write(src)
261		return n, err
262	}
263	return w.Write(formatted)
264}
265
266// Repackage rewrites a Go file from belonging to package main to belonging to
267// the given package.
268func Repackage(inFile, outFile, pkg string) {
269	src, err := ioutil.ReadFile(inFile)
270	if err != nil {
271		log.Fatalf("reading %s: %v", inFile, err)
272	}
273	const toDelete = "package main\n\n"
274	i := bytes.Index(src, []byte(toDelete))
275	if i < 0 {
276		log.Fatalf("Could not find %q in %s.", toDelete, inFile)
277	}
278	w := &bytes.Buffer{}
279	w.Write(src[i+len(toDelete):])
280	WriteGoFile(outFile, pkg, w.Bytes())
281}
282