##// END OF EJS Templates
packaging: establish hgpackaging package...
Gregory Szorc -
r42073:9da97f49 default
parent child Browse files
Show More
1 NO CONTENT: file renamed from contrib/packaging/__init__.py to contrib/packaging/hgpackaging/__init__.py
@@ -1,128 +1,128
1 # packagingutil.py - Common packaging utility code.
1 # util.py - Common packaging utility code.
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import gzip
11 11 import hashlib
12 12 import pathlib
13 13 import tarfile
14 14 import urllib.request
15 15 import zipfile
16 16
17 17
18 18 def hash_path(p: pathlib.Path):
19 19 h = hashlib.sha256()
20 20
21 21 with p.open('rb') as fh:
22 22 while True:
23 23 chunk = fh.read(65536)
24 24 if not chunk:
25 25 break
26 26
27 27 h.update(chunk)
28 28
29 29 return h.hexdigest()
30 30
31 31
32 32 class IntegrityError(Exception):
33 33 """Represents an integrity error when downloading a URL."""
34 34
35 35
36 36 def secure_download_stream(url, size, sha256):
37 37 """Securely download a URL to a stream of chunks.
38 38
39 39 If the integrity of the download fails, an IntegrityError is
40 40 raised.
41 41 """
42 42 h = hashlib.sha256()
43 43 length = 0
44 44
45 45 with urllib.request.urlopen(url) as fh:
46 46 if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip':
47 47 fh = gzip.GzipFile(fileobj=fh)
48 48
49 49 while True:
50 50 chunk = fh.read(65536)
51 51 if not chunk:
52 52 break
53 53
54 54 h.update(chunk)
55 55 length += len(chunk)
56 56
57 57 yield chunk
58 58
59 59 digest = h.hexdigest()
60 60
61 61 if length != size:
62 62 raise IntegrityError('size mismatch on %s: wanted %d; got %d' % (
63 63 url, size, length))
64 64
65 65 if digest != sha256:
66 66 raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % (
67 67 url, sha256, digest))
68 68
69 69
70 70 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
71 71 """Download a URL to a filesystem path, possibly with verification."""
72 72
73 73 # We download to a temporary file and rename at the end so there's
74 74 # no chance of the final file being partially written or containing
75 75 # bad data.
76 76 print('downloading %s to %s' % (url, path))
77 77
78 78 if path.exists():
79 79 good = True
80 80
81 81 if path.stat().st_size != size:
82 82 print('existing file size is wrong; removing')
83 83 good = False
84 84
85 85 if good:
86 86 if hash_path(path) != sha256:
87 87 print('existing file hash is wrong; removing')
88 88 good = False
89 89
90 90 if good:
91 91 print('%s exists and passes integrity checks' % path)
92 92 return
93 93
94 94 path.unlink()
95 95
96 96 tmp = path.with_name('%s.tmp' % path.name)
97 97
98 98 try:
99 99 with tmp.open('wb') as fh:
100 100 for chunk in secure_download_stream(url, size, sha256):
101 101 fh.write(chunk)
102 102 except IntegrityError:
103 103 tmp.unlink()
104 104 raise
105 105
106 106 tmp.rename(path)
107 107 print('successfully downloaded %s' % url)
108 108
109 109
110 110 def download_entry(entry: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path:
111 111 url = entry['url']
112 112
113 113 local_name = local_name or url[url.rindex('/') + 1:]
114 114
115 115 local_path = dest_path / local_name
116 116 download_to_path(url, local_path, entry['size'], entry['sha256'])
117 117
118 118 return local_path
119 119
120 120
121 121 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
122 122 with tarfile.open(source, 'r') as tf:
123 123 tf.extractall(dest)
124 124
125 125
126 126 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
127 127 with zipfile.ZipFile(source, 'r') as zf:
128 128 zf.extractall(dest)
@@ -1,253 +1,253
1 1 #!/usr/bin/env python3
2 2 # build.py - Inno installer build script.
3 3 #
4 4 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 # This script automates the building of the Inno MSI installer for Mercurial.
10 10
11 11 # no-check-code because Python 3 native.
12 12
13 13 import argparse
14 14 import os
15 15 import pathlib
16 16 import shutil
17 17 import subprocess
18 18 import sys
19 19 import tempfile
20 20
21 21
22 22 DOWNLOADS = {
23 23 'gettext': {
24 24 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip',
25 25 'size': 1606131,
26 26 'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641',
27 27 'version': '0.14.4',
28 28 },
29 29 'gettext-dep': {
30 30 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip',
31 31 'size': 715086,
32 32 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
33 33 },
34 34 'py2exe': {
35 35 'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
36 36 'size': 149687,
37 37 'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
38 38 'version': '0.6.9',
39 39 },
40 40 'virtualenv': {
41 41 'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
42 42 'size': 3713208,
43 43 'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
44 44 'version': '16.4.3',
45 45 },
46 46 }
47 47
48 48
49 49 PRINT_PYTHON_INFO = '''
50 50 import platform, sys; print("%s:%d" % (platform.architecture()[0], sys.version_info[0]))
51 51 '''.strip()
52 52
53 53
54 54 def find_vc_runtime_files(x64=False):
55 55 """Finds Visual C++ Runtime DLLs to include in distribution."""
56 56 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
57 57
58 58 prefix = 'amd64' if x64 else 'x86'
59 59
60 60 candidates = sorted(p for p in os.listdir(winsxs)
61 61 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
62 62
63 63 for p in candidates:
64 64 print('found candidate VC runtime: %s' % p)
65 65
66 66 # Take the newest version.
67 67 version = candidates[-1]
68 68
69 69 d = winsxs / version
70 70
71 71 return [
72 72 d / 'msvcm90.dll',
73 73 d / 'msvcp90.dll',
74 74 d / 'msvcr90.dll',
75 75 winsxs / 'Manifests' / ('%s.manifest' % version),
76 76 ]
77 77
78 78
79 79 def build(source_dir: pathlib.Path, build_dir: pathlib.Path,
80 80 python_exe: pathlib.Path, iscc_exe: pathlib.Path,
81 81 version=None):
82 82 """Build the Inno installer.
83 83
84 84 Build files will be placed in ``build_dir``.
85 85
86 86 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
87 87 for finding the Python 2.7 toolchain. So, we require the environment
88 88 to already be configured with an active toolchain.
89 89 """
90 from packagingutil import (
90 from hgpackaging.util import (
91 91 download_entry,
92 92 extract_tar_to_directory,
93 93 extract_zip_to_directory,
94 94 )
95 95
96 96 if not iscc.exists():
97 97 raise Exception('%s does not exist' % iscc)
98 98
99 99 if 'VCINSTALLDIR' not in os.environ:
100 100 raise Exception('not running from a Visual C++ build environment; '
101 101 'execute the "Visual C++ <version> Command Prompt" '
102 102 'application shortcut or a vcsvarsall.bat file')
103 103
104 104 # Identity x86/x64 and validate the environment matches the Python
105 105 # architecture.
106 106 vc_x64 = r'\x64' in os.environ['LIB']
107 107
108 108 res = subprocess.run(
109 109 [str(python_exe), '-c', PRINT_PYTHON_INFO],
110 110 capture_output=True, check=True)
111 111
112 112 py_arch, py_version = res.stdout.decode('utf-8').split(':')
113 113 py_version = int(py_version)
114 114
115 115 if vc_x64:
116 116 if py_arch != '64bit':
117 117 raise Exception('architecture mismatch: Visual C++ environment '
118 118 'is configured for 64-bit but Python is 32-bit')
119 119 else:
120 120 if py_arch != '32bit':
121 121 raise Exception('architecture mismatch: Visual C++ environment '
122 122 'is configured for 32-bit but Python is 64-bit')
123 123
124 124 if py_version != 2:
125 125 raise Exception('Only Python 2 is currently supported')
126 126
127 127 build_dir.mkdir(exist_ok=True)
128 128
129 129 gettext_pkg = download_entry(DOWNLOADS['gettext'], build_dir)
130 130 gettext_dep_pkg = download_entry(DOWNLOADS['gettext-dep'], build_dir)
131 131 virtualenv_pkg = download_entry(DOWNLOADS['virtualenv'], build_dir)
132 132 py2exe_pkg = download_entry(DOWNLOADS['py2exe'], build_dir)
133 133
134 134 venv_path = build_dir / ('venv-inno-%s' % ('x64' if vc_x64 else 'x86'))
135 135
136 136 gettext_root = build_dir / (
137 137 'gettext-win-%s' % DOWNLOADS['gettext']['version'])
138 138
139 139 if not gettext_root.exists():
140 140 extract_zip_to_directory(gettext_pkg, gettext_root)
141 141 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
142 142
143 143 with tempfile.TemporaryDirectory() as td:
144 144 td = pathlib.Path(td)
145 145
146 146 # This assumes Python 2.
147 147 extract_tar_to_directory(virtualenv_pkg, td)
148 148 extract_zip_to_directory(py2exe_pkg, td)
149 149
150 150 virtualenv_src_path = td / ('virtualenv-%s' %
151 151 DOWNLOADS['virtualenv']['version'])
152 152 py2exe_source_path = td / ('py2exe-%s' %
153 153 DOWNLOADS['py2exe']['version'])
154 154
155 155 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
156 156
157 157 if not venv_path.exists():
158 158 print('creating virtualenv with dependencies')
159 159 subprocess.run(
160 160 [str(python_exe), str(virtualenv_py), str(venv_path)],
161 161 check=True)
162 162
163 163 venv_python = venv_path / 'Scripts' / 'python.exe'
164 164 venv_pip = venv_path / 'Scripts' / 'pip.exe'
165 165
166 166 requirements_txt = (source_dir / 'contrib' / 'packaging' /
167 167 'inno' / 'requirements.txt')
168 168 subprocess.run([str(venv_pip), 'install', '-r', str(requirements_txt)],
169 169 check=True)
170 170
171 171 # Force distutils to use VC++ settings from environment, which was
172 172 # validated above.
173 173 env = dict(os.environ)
174 174 env['DISTUTILS_USE_SDK'] = '1'
175 175 env['MSSdk'] = '1'
176 176
177 177 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
178 178 if not py2exe_py_path.exists():
179 179 print('building py2exe')
180 180 subprocess.run([str(venv_python), 'setup.py', 'install'],
181 181 cwd=py2exe_source_path,
182 182 env=env,
183 183 check=True)
184 184
185 185 # Register location of msgfmt and other binaries.
186 186 env['PATH'] = '%s%s%s' % (
187 187 env['PATH'], os.pathsep, str(gettext_root / 'bin'))
188 188
189 189 print('building Mercurial')
190 190 subprocess.run(
191 191 [str(venv_python), 'setup.py',
192 192 'py2exe', '-b', '3' if vc_x64 else '2',
193 193 'build_doc', '--html'],
194 194 cwd=str(source_dir),
195 195 env=env,
196 196 check=True)
197 197
198 198 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
199 199 for f in find_vc_runtime_files(vc_x64):
200 200 if f.name.endswith('.manifest'):
201 201 basename = 'Microsoft.VC90.CRT.manifest'
202 202 else:
203 203 basename = f.name
204 204
205 205 dest_path = source_dir / 'dist' / basename
206 206
207 207 print('copying %s to %s' % (f, dest_path))
208 208 shutil.copyfile(f, dest_path)
209 209
210 210 print('creating installer')
211 211
212 212 args = [str(iscc_exe)]
213 213
214 214 if vc_x64:
215 215 args.append('/dARCH=x64')
216 216
217 217 if version:
218 218 args.append('/dVERSION=%s' % version)
219 219
220 220 args.append('/Odist')
221 221 args.append('contrib/packaging/inno/mercurial.iss')
222 222
223 223 subprocess.run(args, cwd=str(source_dir), check=True)
224 224
225 225
226 226 if __name__ == '__main__':
227 227 parser = argparse.ArgumentParser()
228 228
229 229 parser.add_argument('--python',
230 230 required=True,
231 231 help='path to python.exe to use')
232 232 parser.add_argument('--iscc',
233 233 help='path to iscc.exe to use')
234 234 parser.add_argument('--version',
235 235 help='Mercurial version string to use '
236 236 '(detected from __version__.py if not defined')
237 237
238 238 args = parser.parse_args()
239 239
240 240 if args.iscc:
241 241 iscc = pathlib.Path(args.iscc)
242 242 else:
243 243 iscc = (pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Inno Setup 5' /
244 244 'ISCC.exe')
245 245
246 246 here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
247 247 source_dir = here.parent.parent.parent
248 248 build_dir = source_dir / 'build'
249 249
250 250 sys.path.insert(0, str(source_dir / 'contrib' / 'packaging'))
251 251
252 252 build(source_dir, build_dir, pathlib.Path(args.python), iscc,
253 253 version=args.version)
@@ -1,71 +1,71
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4 $ check_code="$TESTDIR"/../contrib/check-code.py
5 5 $ cd "$TESTDIR"/..
6 6
7 7 New errors are not allowed. Warnings are strongly discouraged.
8 8 (The writing "no-che?k-code" is for not skipping this file when checking.)
9 9
10 10 $ testrepohg locate \
11 11 > -X contrib/python-zstandard \
12 12 > -X hgext/fsmonitor/pywatchman \
13 13 > -X mercurial/thirdparty \
14 14 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
15 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
15 16 Skipping contrib/packaging/inno/build.py it has no-che?k-code (glob)
16 Skipping contrib/packaging/packagingutil.py it has no-che?k-code (glob)
17 17 Skipping i18n/polib.py it has no-che?k-code (glob)
18 18 Skipping mercurial/statprof.py it has no-che?k-code (glob)
19 19 Skipping tests/badserverext.py it has no-che?k-code (glob)
20 20
21 21 @commands in debugcommands.py should be in alphabetical order.
22 22
23 23 >>> import re
24 24 >>> commands = []
25 25 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
26 26 ... for line in fh:
27 27 ... m = re.match(br"^@command\('([a-z]+)", line)
28 28 ... if m:
29 29 ... commands.append(m.group(1))
30 30 >>> scommands = list(sorted(commands))
31 31 >>> for i, command in enumerate(scommands):
32 32 ... if command != commands[i]:
33 33 ... print('commands in debugcommands.py not sorted; first differing '
34 34 ... 'command is %s; expected %s' % (commands[i], command))
35 35 ... break
36 36
37 37 Prevent adding new files in the root directory accidentally.
38 38
39 39 $ testrepohg files 'glob:*'
40 40 .arcconfig
41 41 .clang-format
42 42 .editorconfig
43 43 .hgignore
44 44 .hgsigs
45 45 .hgtags
46 46 .jshintrc
47 47 CONTRIBUTING
48 48 CONTRIBUTORS
49 49 COPYING
50 50 Makefile
51 51 README.rst
52 52 hg
53 53 hgeditor
54 54 hgweb.cgi
55 55 setup.py
56 56
57 57 Prevent adding modules which could be shadowed by ancient .so/.dylib.
58 58
59 59 $ testrepohg files \
60 60 > mercurial/base85.py \
61 61 > mercurial/bdiff.py \
62 62 > mercurial/diffhelpers.py \
63 63 > mercurial/mpatch.py \
64 64 > mercurial/osutil.py \
65 65 > mercurial/parsers.py \
66 66 > mercurial/zstd.py
67 67 [1]
68 68
69 69 Keep python3 tests sorted:
70 70 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
71 71 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
@@ -1,38 +1,38
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4 $ import_checker="$TESTDIR"/../contrib/import-checker.py
5 5
6 6 $ cd "$TESTDIR"/..
7 7
8 8 There are a handful of cases here that require renaming a module so it
9 9 doesn't overlap with a stdlib module name. There are also some cycles
10 10 here that we should still endeavor to fix, and some cycles will be
11 11 hidden by deduplication algorithm in the cycle detector, so fixing
12 12 these may expose other cycles.
13 13
14 14 Known-bad files are excluded by -X as some of them would produce unstable
15 15 outputs, which should be fixed later.
16 16
17 17 $ testrepohg locate 'set:**.py or grep(r"^#!.*?python")' \
18 18 > 'tests/**.t' \
19 19 > -X hgweb.cgi \
20 20 > -X setup.py \
21 21 > -X contrib/debugshell.py \
22 22 > -X contrib/hgweb.fcgi \
23 23 > -X contrib/packaging/hg-docker \
24 > -X contrib/packaging/hgpackaging/ \
24 25 > -X contrib/packaging/inno/ \
25 > -X contrib/packaging/packagingutil.py \
26 26 > -X contrib/python-zstandard/ \
27 27 > -X contrib/win32/hgwebdir_wsgi.py \
28 28 > -X contrib/perf-utils/perf-revlog-write-plot.py \
29 29 > -X doc/gendoc.py \
30 30 > -X doc/hgmanpage.py \
31 31 > -X i18n/posplit \
32 32 > -X mercurial/thirdparty \
33 33 > -X tests/hypothesishelpers.py \
34 34 > -X tests/test-check-interfaces.py \
35 35 > -X tests/test-demandimport.py \
36 36 > -X tests/test-imports-checker.t \
37 37 > -X tests/test-verify-repo-operations.py \
38 38 > | sed 's-\\-/-g' | "$PYTHON" "$import_checker" -
@@ -1,58 +1,58
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4 $ cd "$TESTDIR"/..
5 5
6 6 #if no-py3
7 7 $ testrepohg files 'set:(**.py)' \
8 > -X contrib/packaging/hgpackaging/ \
8 9 > -X contrib/packaging/inno/ \
9 > -X contrib/packaging/packagingutil.py \
10 10 > -X hgdemandimport/demandimportpy2.py \
11 11 > -X mercurial/thirdparty/cbor \
12 12 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py
13 13 contrib/python-zstandard/setup.py not using absolute_import
14 14 contrib/python-zstandard/setup_zstd.py not using absolute_import
15 15 contrib/python-zstandard/tests/common.py not using absolute_import
16 16 contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
17 17 contrib/python-zstandard/tests/test_compressor.py not using absolute_import
18 18 contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
19 19 contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
20 20 contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
21 21 contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
22 22 contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
23 23 contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
24 24 contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
25 25 contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
26 26 setup.py not using absolute_import
27 27 #endif
28 28
29 29 #if py3
30 30 $ testrepohg files 'set:(**.py) - grep(pygments)' \
31 31 > -X hgdemandimport/demandimportpy2.py \
32 32 > -X hgext/fsmonitor/pywatchman \
33 33 > -X mercurial/cffi \
34 34 > -X mercurial/thirdparty \
35 35 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py \
36 36 > | sed 's/[0-9][0-9]*)$/*)/'
37 37 contrib/python-zstandard/tests/test_compressor.py:324: SyntaxWarning: invalid escape sequence \( (py38 !)
38 38 with self.assertRaisesRegexp(zstd.ZstdError, 'cannot call compress\(\) after compressor'): (py38 !)
39 39 contrib/python-zstandard/tests/test_compressor.py:1329: SyntaxWarning: invalid escape sequence \( (py38 !)
40 40 'cannot call compress\(\) after compression finished'): (py38 !)
41 41 contrib/python-zstandard/tests/test_compressor.py:1341: SyntaxWarning: invalid escape sequence \( (py38 !)
42 42 'cannot call flush\(\) after compression finished'): (py38 !)
43 43 contrib/python-zstandard/tests/test_compressor.py:1353: SyntaxWarning: invalid escape sequence \( (py38 !)
44 44 'cannot call finish\(\) after compression finished'): (py38 !)
45 45 hgext/convert/transport.py: error importing: <*Error> No module named 'svn.client' (error at transport.py:*) (glob) (?)
46 46 hgext/infinitepush/sqlindexapi.py: error importing: <*Error> No module named 'mysql' (error at sqlindexapi.py:*) (glob) (?)
47 47 mercurial/scmwindows.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
48 48 mercurial/win32.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
49 49 mercurial/windows.py: error importing: <ModuleNotFoundError> No module named 'msvcrt' (error at windows.py:*) (no-windows !)
50 50 mercurial/posix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at posix.py:*) (windows !)
51 51 mercurial/scmposix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at scmposix.py:*) (windows !)
52 52 #endif
53 53
54 54 #if py3 pygments
55 55 $ testrepohg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
56 56 > | xargs "$PYTHON" contrib/check-py3-compat.py \
57 57 > | sed 's/[0-9][0-9]*)$/*)/'
58 58 #endif
General Comments 0
You need to be logged in to leave comments. Login now