Skip to content

Commit 7cb4aad

Browse files
committed
build: i18n: Autodownload ICU, Add a test.
This is to implement nodejs#7676 (comment) * make `--with-intl=none` the default * Download, verify (md5), unpack ICU's zip if not there * update docs * add a test There's a "list" of URLs being used, but right now only the first is picked up. The logic works something like this: * if there is no directory `deps/icu`, * if no zip file (currently `icu4c-54_1-src.zip`), * download zip file (icu-project.org -> sf.net) * verify the MD5 sum of the zipfile * if bad, print error and exit * unpack the zipfile into `deps/icu` * if `deps/icu` now exists, use it, else fail with help text Also: * refactor some code into tools/configure.d/nodedownload.py * add `intl-none` option for `vcbuild.bat` To rebuild `deps/icu-small` - (not currently checked in) ``` bash tools/icu/prepare-icu-source.sh ``` Reduce space by about 1MB with ICU 54 (over without this patch). Also trims a few other source files, but only conditional on the exact ICU version used. This is to future-proof - a file that is unneeded now may be needed in future ICUs.
1 parent 4bba870 commit 7cb4aad

File tree

10 files changed

+415
-19
lines changed

10 files changed

+415
-19
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ ipch/
4646
email.md
4747
deps/v8-*
4848
deps/icu
49+
deps/icu*.zip
50+
deps/icu*.tgz
4951
./node_modules
5052
.svn/
5153

README.md

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,76 @@ make doc
8383
man doc/node.1
8484
```
8585

86-
### To build `Intl` (ECMA-402) support:
86+
### `Intl` (ECMA-402) support:
8787

88-
*Note:* more docs, including how to reduce disk footprint, are on
88+
[Intl](https://github.com/joyent/node/wiki/Intl) support is not
89+
enabled by default.
90+
91+
#### "small" (English only) support
92+
93+
This option will build with "small" (English only) support, but
94+
the full `Intl` (ECMA-402) APIs. It will download the ICU library
95+
as needed.
96+
97+
Unix/Macintosh:
98+
99+
```sh
100+
./configure --with-intl=small-icu
101+
```
102+
103+
Windows:
104+
105+
```sh
106+
vcbuild small-icu
107+
```
108+
109+
The `small-icu` mode builds
110+
with English-only data. You can add full data at runtime.
111+
112+
*Note:* more docs are on
89113
[the wiki](https://github.com/joyent/node/wiki/Intl).
90114

115+
#### Build with full ICU support (all locales supported by ICU):
116+
117+
*Note*, this may download ICU if you don't have an ICU in `deps/icu`
118+
119+
Unix/Macintosh:
120+
121+
```sh
122+
./configure --with-intl=full-icu
123+
```
124+
125+
Windows:
126+
127+
```sh
128+
vcbuild full-icu
129+
```
130+
131+
#### Build with no Intl support `:-(`
132+
133+
The `Intl` object will not be available.
134+
135+
Unix/Macintosh:
136+
137+
```sh
138+
./configure --with-intl=none
139+
```
140+
141+
Windows:
142+
143+
```sh
144+
vcbuild intl-none
145+
```
146+
91147
#### Use existing installed ICU (Unix/Macintosh only):
92148

93149
```sh
94150
pkg-config --modversion icu-i18n && ./configure --with-intl=system-icu
95151
```
96152

97-
#### Build ICU from source:
153+
#### Build with a specific ICU:
98154

99-
First: Unpack latest ICU
155+
First: Unpack latest ICU to `deps/icu`
100156
[icu4c-**##.#**-src.tgz](http://icu-project.org/download) (or `.zip`)
101157
as `deps/icu` (You'll have: `deps/icu/source/...`)
102158

configure

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@ import re
66
import shlex
77
import subprocess
88
import sys
9+
import shutil
910

1011
CC = os.environ.get('CC', 'cc')
1112

1213
root_dir = os.path.dirname(__file__)
1314
sys.path.insert(0, os.path.join(root_dir, 'tools', 'gyp', 'pylib'))
1415
from gyp.common import GetFlavor
1516

17+
# imports in tools/configure.d
18+
sys.path.insert(0, os.path.join(root_dir, 'tools', 'configure.d'))
19+
import nodedownload
20+
1621
# parse our options
1722
parser = optparse.OptionParser()
1823

@@ -712,6 +717,34 @@ def glob_to_var(dir_base, dir_sub):
712717
return list
713718

714719
def configure_intl(o):
720+
icus = [
721+
{
722+
'url': 'http://download.icu-project.org/files/icu4c/54.1/icu4c-54_1-src.zip',
723+
# from https://ssl.icu-project.org/files/icu4c/54.1/icu4c-src-54_1.md5:
724+
'md5': '6b89d60e2f0e140898ae4d7f72323bca',
725+
},
726+
]
727+
def icu_download(path):
728+
# download ICU, if needed
729+
for icu in icus:
730+
url = icu['url']
731+
md5 = icu['md5']
732+
local = url.split('/')[-1]
733+
targetfile = os.path.join(root_dir, 'deps', local)
734+
if not os.path.isfile(targetfile):
735+
nodedownload.retrievefile(url, targetfile)
736+
else:
737+
print ' Re-using existing %s' % targetfile
738+
if os.path.isfile(targetfile):
739+
sys.stdout.write(' Checking file integrity with MD5:\r')
740+
gotmd5 = nodedownload.md5sum(targetfile)
741+
print ' MD5: %s %s' % (gotmd5, targetfile)
742+
if (md5 == gotmd5):
743+
return targetfile
744+
else:
745+
print ' Expected: %s *MISMATCH*' % md5
746+
print '\n ** Corrupted ZIP? Delete %s to retry download.\n' % targetfile
747+
return None
715748
icu_config = {
716749
'variables': {}
717750
}
@@ -723,7 +756,6 @@ def configure_intl(o):
723756
write(icu_config_name, do_not_edit +
724757
pprint.pformat(icu_config, indent=2) + '\n')
725758

726-
# small ICU is off by default.
727759
# always set icu_small, node.gyp depends on it being defined.
728760
o['variables']['icu_small'] = b(False)
729761

@@ -739,6 +771,8 @@ def configure_intl(o):
739771
o['variables']['icu_gyp_path'] = options.with_icu_path
740772
return
741773
# --with-intl=<with_intl>
774+
if with_intl is None:
775+
with_intl = 'none' # The default mode of Intl
742776
if with_intl == 'none' or with_intl is None:
743777
o['variables']['v8_enable_i18n_support'] = 0
744778
return # no Intl
@@ -769,20 +803,45 @@ def configure_intl(o):
769803
# Note: non-ICU implementations could use other 'with_intl'
770804
# values.
771805

806+
icu_parent_path = os.path.join(root_dir, 'deps')
807+
icu_full_path = os.path.join(icu_parent_path, 'icu')
808+
icu_small_path = os.path.join(icu_parent_path, 'icu-small')
809+
icu_small_tag = os.path.join(icu_full_path, 'is-small-icu.txt')
810+
811+
## Use (or not) an embedded small-icu.
812+
if with_intl == 'small-icu':
813+
if not os.path.isdir(icu_full_path) and os.path.isdir(icu_small_path):
814+
# deps/small-icu -> deps/icu
815+
print 'Copying small ICU %s to %s' % (icu_small_path, icu_full_path)
816+
shutil.copytree(icu_small_path, icu_full_path)
817+
#else:
818+
# print 'Not copying %s to %s' % (icu_small_path, icu_full_path)
819+
elif os.path.isfile(icu_small_tag):
820+
print 'deleting small-icu %s for --with-intl=%s' % (icu_full_path, with_intl)
821+
shutil.rmtree(icu_full_path)
822+
772823
# ICU mode. (icu-generic.gyp)
773824
byteorder = sys.byteorder
774825
o['variables']['icu_gyp_path'] = 'tools/icu/icu-generic.gyp'
775826
# ICU source dir relative to root
776-
icu_full_path = os.path.join(root_dir, 'deps/icu')
777827
o['variables']['icu_path'] = icu_full_path
778828
if not os.path.isdir(icu_full_path):
779-
print 'Error: ICU path is not a directory: %s' % (icu_full_path)
829+
print '* ECMA-402 (Intl) support didn\'t find ICU in %s..' % (icu_full_path)
830+
# can we download (or find) a zipfile?
831+
localzip = icu_download(icu_full_path)
832+
if localzip:
833+
nodedownload.unpack(localzip, icu_parent_path)
834+
if not os.path.isdir(icu_full_path):
835+
print ' Cannot build Intl without ICU in %s.' % (icu_full_path)
836+
print ' (Fix, or disable with "--with-intl=none" )'
780837
sys.exit(1)
838+
else:
839+
print '* Using ICU in %s' % (icu_full_path)
781840
# Now, what version of ICU is it? We just need the "major", such as 54.
782841
# uvernum.h contains it as a #define.
783842
uvernum_h = os.path.join(icu_full_path, 'source/common/unicode/uvernum.h')
784843
if not os.path.isfile(uvernum_h):
785-
print 'Error: could not load %s - is ICU installed?' % uvernum_h
844+
print ' Error: could not load %s - is ICU installed?' % uvernum_h
786845
sys.exit(1)
787846
icu_ver_major = None
788847
matchVerExp = r'^\s*#define\s+U_ICU_VERSION_SHORT\s+"([^"]*)".*'
@@ -792,7 +851,7 @@ def configure_intl(o):
792851
if m:
793852
icu_ver_major = m.group(1)
794853
if not icu_ver_major:
795-
print 'Could not read U_ICU_VERSION_SHORT version from %s' % uvernum_h
854+
print ' Could not read U_ICU_VERSION_SHORT version from %s' % uvernum_h
796855
sys.exit(1)
797856
icu_endianness = sys.byteorder[0]; # TODO(srl295): EBCDIC should be 'e'
798857
o['variables']['icu_ver_major'] = icu_ver_major
@@ -819,8 +878,8 @@ def configure_intl(o):
819878
# this is the icudt*.dat file which node will be using (platform endianness)
820879
o['variables']['icu_data_file'] = icu_data_file
821880
if not os.path.isfile(icu_data_path):
822-
print 'Error: ICU prebuilt data file %s does not exist.' % icu_data_path
823-
print 'See the README.md.'
881+
print ' Error: ICU prebuilt data file %s does not exist.' % icu_data_path
882+
print ' See the README.md.'
824883
# .. and we're not about to build it from .gyp!
825884
sys.exit(1)
826885
# map from variable name to subdirs

test/simple/test-intl.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var assert = require('assert');
24+
25+
var enablei18n = process.config.variables.v8_enable_i18n_support;
26+
if (enablei18n === undefined) {
27+
enablei18n = false;
28+
}
29+
30+
var haveIntl = ( global.Intl != undefined );
31+
32+
if (!haveIntl) {
33+
assert.equal(enablei18n, false, '"Intl" object is NOT present but v8_enable_i18n_support is ' + enablei18n);
34+
console.log('Skipping Intl tests because Intl object not present.');
35+
} else {
36+
assert.equal(enablei18n, true, '"Intl" object is present but v8_enable_i18n_support is ' + enablei18n + '. Is this test out of date?');
37+
38+
// Check with toLocaleString
39+
var date0 = new Date(0);
40+
var GMT = 'Etc/GMT';
41+
var optsGMT = {timeZone: GMT};
42+
var localeString0 = date0.toLocaleString(['en'], optsGMT);
43+
var expectString0 = '1/1/1970, 12:00:00 AM'; // epoch
44+
assert.equal(localeString0, expectString0);
45+
46+
// check with a Formatter
47+
var dtf = new Intl.DateTimeFormat(['en'], {timeZone: GMT, month: 'short', year: '2-digit'});
48+
var localeString1 = dtf.format(date0);
49+
assert.equal(localeString1, 'Jan 70');
50+
51+
// number format
52+
assert.equal(new Intl.NumberFormat(['en']).format(12345.67890), '12,345.679');
53+
54+
var coll = new Intl.Collator(['en'],{sensitivity:'base',ignorePunctuation:true});
55+
56+
assert.equal(coll.compare('blackbird', 'black-bird'), 0, 'ignore punctuation failed');
57+
58+
assert.equal(coll.compare('blackbird', 'red-bird'), -1, 'compare less failed');
59+
assert.equal(coll.compare('bluebird', 'blackbird'), 1, 'compare greater failed');
60+
assert.equal(coll.compare('Bluebird', 'bluebird'), 0, 'ignore case failed');
61+
assert.equal(coll.compare('\ufb03', 'ffi'), 0, 'ffi ligature (contraction) failed');
62+
}

tools/configure.d/nodedownload.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python
2+
# Moved some utilities here from ../../configure
3+
4+
import urllib
5+
import hashlib
6+
import sys
7+
import zipfile
8+
9+
def formatSize(amt):
10+
"""Format a size as a string in MB"""
11+
return "{:.1f}".format(amt / 1024000.)
12+
13+
def spin(c):
14+
"""print out a spinner based on 'c'"""
15+
# spin = "\\|/-"
16+
spin = ".:|'"
17+
return (spin[c % len(spin)])
18+
19+
class ConfigOpener(urllib.FancyURLopener):
20+
"""fancy opener used by retrievefile. Set a UA"""
21+
# append to existing version (UA)
22+
version = '%s node.js/configure' % urllib.URLopener.version
23+
24+
def reporthook(count, size, total):
25+
"""internal hook used by retrievefile"""
26+
sys.stdout.write(' Fetch: %c %sMB total, %sMB downloaded \r' %
27+
(spin(count),
28+
formatSize(total),
29+
formatSize(count*size)))
30+
31+
def retrievefile(url, targetfile):
32+
"""fetch file 'url' as 'targetfile'. Return targetfile or throw."""
33+
try:
34+
sys.stdout.write(' <%s>\nConnecting...\r' % url)
35+
sys.stdout.flush()
36+
msg = ConfigOpener().retrieve(url, targetfile, reporthook=reporthook)
37+
print '' # clear the line
38+
return targetfile
39+
except:
40+
print ' ** Error occurred while downloading\n <%s>' % url
41+
raise
42+
43+
def md5sum(targetfile):
44+
"""md5sum a file. Return the hex digest."""
45+
digest = hashlib.md5()
46+
with open(targetfile, 'rb') as f:
47+
chunk = f.read(1024)
48+
while chunk != "":
49+
digest.update(chunk)
50+
chunk = f.read(1024)
51+
return digest.hexdigest()
52+
53+
def unpack(packedfile, parent_path):
54+
"""Unpack packedfile into parent_path. Assumes .zip."""
55+
with zipfile.ZipFile(packedfile, 'r') as icuzip:
56+
print ' Extracting source zip: %s' % packedfile
57+
icuzip.extractall(parent_path)

0 commit comments

Comments
 (0)