##// END OF EJS Templates
packaging: convert files to LF...
Gregory Szorc -
r42118:b83de915 default
parent child Browse files
Show More
@@ -1,175 +1,175
1 # downloads.py - Code for downloading dependencies.
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import gzip
11 import hashlib
12 import pathlib
13 import urllib.request
14
15
16 DOWNLOADS = {
17 'gettext': {
18 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip',
19 'size': 1606131,
20 'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641',
21 'version': '0.14.4',
22 },
23 'gettext-dep': {
24 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip',
25 'size': 715086,
26 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
27 },
28 'py2exe': {
29 'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
30 'size': 149687,
31 'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
32 'version': '0.6.9',
33 },
34 # The VC9 CRT merge modules aren't readily available on most systems because
35 # they are only installed as part of a full Visual Studio 2008 install.
36 # While we could potentially extract them from a Visual Studio 2008
37 # installer, it is easier to just fetch them from a known URL.
38 'vc9-crt-x86-msm': {
39 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86.msm',
40 'size': 615424,
41 'sha256': '837e887ef31b332feb58156f429389de345cb94504228bb9a523c25a9dd3d75e',
42 },
43 'vc9-crt-x86-msm-policy': {
44 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86.msm',
45 'size': 71168,
46 'sha256': '3fbcf92e3801a0757f36c5e8d304e134a68d5cafd197a6df7734ae3e8825c940',
47 },
48 'vc9-crt-x64-msm': {
49 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86_x64.msm',
50 'size': 662528,
51 'sha256': '50d9639b5ad4844a2285269c7551bf5157ec636e32396ddcc6f7ec5bce487a7c',
52 },
53 'vc9-crt-x64-msm-policy': {
54 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86_x64.msm',
55 'size': 71168,
56 'sha256': '0550ea1929b21239134ad3a678c944ba0f05f11087117b6cf0833e7110686486',
57 },
58 'virtualenv': {
59 'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
60 'size': 3713208,
61 'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
62 'version': '16.4.3',
63 },
64 'wix': {
65 'url': 'https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip',
66 'size': 34358269,
67 'sha256': '37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d',
68 'version': '3.11.1',
69 },
70 }
71
72
73 def hash_path(p: pathlib.Path):
74 h = hashlib.sha256()
75
76 with p.open('rb') as fh:
77 while True:
78 chunk = fh.read(65536)
79 if not chunk:
80 break
81
82 h.update(chunk)
83
84 return h.hexdigest()
85
86
87 class IntegrityError(Exception):
88 """Represents an integrity error when downloading a URL."""
89
90
91 def secure_download_stream(url, size, sha256):
92 """Securely download a URL to a stream of chunks.
93
94 If the integrity of the download fails, an IntegrityError is
95 raised.
96 """
97 h = hashlib.sha256()
98 length = 0
99
100 with urllib.request.urlopen(url) as fh:
101 if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip':
102 fh = gzip.GzipFile(fileobj=fh)
103
104 while True:
105 chunk = fh.read(65536)
106 if not chunk:
107 break
108
109 h.update(chunk)
110 length += len(chunk)
111
112 yield chunk
113
114 digest = h.hexdigest()
115
116 if length != size:
117 raise IntegrityError('size mismatch on %s: wanted %d; got %d' % (
118 url, size, length))
119
120 if digest != sha256:
121 raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % (
122 url, sha256, digest))
123
124
125 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
126 """Download a URL to a filesystem path, possibly with verification."""
127
128 # We download to a temporary file and rename at the end so there's
129 # no chance of the final file being partially written or containing
130 # bad data.
131 print('downloading %s to %s' % (url, path))
132
133 if path.exists():
134 good = True
135
136 if path.stat().st_size != size:
137 print('existing file size is wrong; removing')
138 good = False
139
140 if good:
141 if hash_path(path) != sha256:
142 print('existing file hash is wrong; removing')
143 good = False
144
145 if good:
146 print('%s exists and passes integrity checks' % path)
147 return
148
149 path.unlink()
150
151 tmp = path.with_name('%s.tmp' % path.name)
152
153 try:
154 with tmp.open('wb') as fh:
155 for chunk in secure_download_stream(url, size, sha256):
156 fh.write(chunk)
157 except IntegrityError:
158 tmp.unlink()
159 raise
160
161 tmp.rename(path)
162 print('successfully downloaded %s' % url)
163
164
165 def download_entry(name: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path:
166 entry = DOWNLOADS[name]
167
168 url = entry['url']
169
170 local_name = local_name or url[url.rindex('/') + 1:]
171
172 local_path = dest_path / local_name
173 download_to_path(url, local_path, entry['size'], entry['sha256'])
174
175 return local_path, entry
1 # downloads.py - Code for downloading dependencies.
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import gzip
11 import hashlib
12 import pathlib
13 import urllib.request
14
15
16 DOWNLOADS = {
17 'gettext': {
18 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-bin.zip',
19 'size': 1606131,
20 'sha256': '60b9ef26bc5cceef036f0424e542106cf158352b2677f43a01affd6d82a1d641',
21 'version': '0.14.4',
22 },
23 'gettext-dep': {
24 'url': 'https://versaweb.dl.sourceforge.net/project/gnuwin32/gettext/0.14.4/gettext-0.14.4-dep.zip',
25 'size': 715086,
26 'sha256': '411f94974492fd2ecf52590cb05b1023530aec67e64154a88b1e4ebcd9c28588',
27 },
28 'py2exe': {
29 'url': 'https://versaweb.dl.sourceforge.net/project/py2exe/py2exe/0.6.9/py2exe-0.6.9.zip',
30 'size': 149687,
31 'sha256': '6bd383312e7d33eef2e43a5f236f9445e4f3e0f6b16333c6f183ed445c44ddbd',
32 'version': '0.6.9',
33 },
34 # The VC9 CRT merge modules aren't readily available on most systems because
35 # they are only installed as part of a full Visual Studio 2008 install.
36 # While we could potentially extract them from a Visual Studio 2008
37 # installer, it is easier to just fetch them from a known URL.
38 'vc9-crt-x86-msm': {
39 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86.msm',
40 'size': 615424,
41 'sha256': '837e887ef31b332feb58156f429389de345cb94504228bb9a523c25a9dd3d75e',
42 },
43 'vc9-crt-x86-msm-policy': {
44 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86.msm',
45 'size': 71168,
46 'sha256': '3fbcf92e3801a0757f36c5e8d304e134a68d5cafd197a6df7734ae3e8825c940',
47 },
48 'vc9-crt-x64-msm': {
49 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/Microsoft_VC90_CRT_x86_x64.msm',
50 'size': 662528,
51 'sha256': '50d9639b5ad4844a2285269c7551bf5157ec636e32396ddcc6f7ec5bce487a7c',
52 },
53 'vc9-crt-x64-msm-policy': {
54 'url': 'https://github.com/indygreg/vc90-merge-modules/raw/9232f8f0b2135df619bf7946eaa176b4ac35ccff/policy_9_0_Microsoft_VC90_CRT_x86_x64.msm',
55 'size': 71168,
56 'sha256': '0550ea1929b21239134ad3a678c944ba0f05f11087117b6cf0833e7110686486',
57 },
58 'virtualenv': {
59 'url': 'https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/virtualenv-16.4.3.tar.gz',
60 'size': 3713208,
61 'sha256': '984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39',
62 'version': '16.4.3',
63 },
64 'wix': {
65 'url': 'https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip',
66 'size': 34358269,
67 'sha256': '37f0a533b0978a454efb5dc3bd3598becf9660aaf4287e55bf68ca6b527d051d',
68 'version': '3.11.1',
69 },
70 }
71
72
73 def hash_path(p: pathlib.Path):
74 h = hashlib.sha256()
75
76 with p.open('rb') as fh:
77 while True:
78 chunk = fh.read(65536)
79 if not chunk:
80 break
81
82 h.update(chunk)
83
84 return h.hexdigest()
85
86
87 class IntegrityError(Exception):
88 """Represents an integrity error when downloading a URL."""
89
90
91 def secure_download_stream(url, size, sha256):
92 """Securely download a URL to a stream of chunks.
93
94 If the integrity of the download fails, an IntegrityError is
95 raised.
96 """
97 h = hashlib.sha256()
98 length = 0
99
100 with urllib.request.urlopen(url) as fh:
101 if not url.endswith('.gz') and fh.info().get('Content-Encoding') == 'gzip':
102 fh = gzip.GzipFile(fileobj=fh)
103
104 while True:
105 chunk = fh.read(65536)
106 if not chunk:
107 break
108
109 h.update(chunk)
110 length += len(chunk)
111
112 yield chunk
113
114 digest = h.hexdigest()
115
116 if length != size:
117 raise IntegrityError('size mismatch on %s: wanted %d; got %d' % (
118 url, size, length))
119
120 if digest != sha256:
121 raise IntegrityError('sha256 mismatch on %s: wanted %s; got %s' % (
122 url, sha256, digest))
123
124
125 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
126 """Download a URL to a filesystem path, possibly with verification."""
127
128 # We download to a temporary file and rename at the end so there's
129 # no chance of the final file being partially written or containing
130 # bad data.
131 print('downloading %s to %s' % (url, path))
132
133 if path.exists():
134 good = True
135
136 if path.stat().st_size != size:
137 print('existing file size is wrong; removing')
138 good = False
139
140 if good:
141 if hash_path(path) != sha256:
142 print('existing file hash is wrong; removing')
143 good = False
144
145 if good:
146 print('%s exists and passes integrity checks' % path)
147 return
148
149 path.unlink()
150
151 tmp = path.with_name('%s.tmp' % path.name)
152
153 try:
154 with tmp.open('wb') as fh:
155 for chunk in secure_download_stream(url, size, sha256):
156 fh.write(chunk)
157 except IntegrityError:
158 tmp.unlink()
159 raise
160
161 tmp.rename(path)
162 print('successfully downloaded %s' % url)
163
164
165 def download_entry(name: dict, dest_path: pathlib.Path, local_name=None) -> pathlib.Path:
166 entry = DOWNLOADS[name]
167
168 url = entry['url']
169
170 local_name = local_name or url[url.rindex('/') + 1:]
171
172 local_path = dest_path / local_name
173 download_to_path(url, local_path, entry['size'], entry['sha256'])
174
175 return local_path, entry
@@ -1,157 +1,157
1 # util.py - Common packaging utility code.
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import distutils.version
11 import getpass
12 import os
13 import pathlib
14 import subprocess
15 import tarfile
16 import zipfile
17
18
19 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
20 with tarfile.open(source, 'r') as tf:
21 tf.extractall(dest)
22
23
24 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
25 with zipfile.ZipFile(source, 'r') as zf:
26 zf.extractall(dest)
27
28
29 def find_vc_runtime_files(x64=False):
30 """Finds Visual C++ Runtime DLLs to include in distribution."""
31 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
32
33 prefix = 'amd64' if x64 else 'x86'
34
35 candidates = sorted(p for p in os.listdir(winsxs)
36 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
37
38 for p in candidates:
39 print('found candidate VC runtime: %s' % p)
40
41 # Take the newest version.
42 version = candidates[-1]
43
44 d = winsxs / version
45
46 return [
47 d / 'msvcm90.dll',
48 d / 'msvcp90.dll',
49 d / 'msvcr90.dll',
50 winsxs / 'Manifests' / ('%s.manifest' % version),
51 ]
52
53
54 def windows_10_sdk_info():
55 """Resolves information about the Windows 10 SDK."""
56
57 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
58
59 if not base.is_dir():
60 raise Exception('unable to find Windows 10 SDK at %s' % base)
61
62 # Find the latest version.
63 bin_base = base / 'bin'
64
65 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
66 version = sorted(versions, reverse=True)[0]
67
68 bin_version = bin_base / version
69
70 return {
71 'root': base,
72 'version': version,
73 'bin_root': bin_version,
74 'bin_x86': bin_version / 'x86',
75 'bin_x64': bin_version / 'x64'
76 }
77
78
79 def find_signtool():
80 """Find signtool.exe from the Windows SDK."""
81 sdk = windows_10_sdk_info()
82
83 for key in ('bin_x64', 'bin_x86'):
84 p = sdk[key] / 'signtool.exe'
85
86 if p.exists():
87 return p
88
89 raise Exception('could not find signtool.exe in Windows 10 SDK')
90
91
92 def sign_with_signtool(file_path, description, subject_name=None,
93 cert_path=None, cert_password=None,
94 timestamp_url=None):
95 """Digitally sign a file with signtool.exe.
96
97 ``file_path`` is file to sign.
98 ``description`` is text that goes in the signature.
99
100 The signing certificate can be specified by ``cert_path`` or
101 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
102 to signtool.exe, respectively.
103
104 The certificate password can be specified via ``cert_password``. If
105 not provided, you will be prompted for the password.
106
107 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
108 argument to signtool.exe).
109 """
110 if cert_path and subject_name:
111 raise ValueError('cannot specify both cert_path and subject_name')
112
113 while cert_path and not cert_password:
114 cert_password = getpass.getpass('password for %s: ' % cert_path)
115
116 args = [
117 str(find_signtool()), 'sign',
118 '/v',
119 '/fd', 'sha256',
120 '/d', description,
121 ]
122
123 if cert_path:
124 args.extend(['/f', str(cert_path), '/p', cert_password])
125 elif subject_name:
126 args.extend(['/n', subject_name])
127
128 if timestamp_url:
129 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
130
131 args.append(str(file_path))
132
133 print('signing %s' % file_path)
134 subprocess.run(args, check=True)
135
136
137 PRINT_PYTHON_INFO = '''
138 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
139 '''.strip()
140
141
142 def python_exe_info(python_exe: pathlib.Path):
143 """Obtain information about a Python executable."""
144
145 res = subprocess.run(
146 [str(python_exe), '-c', PRINT_PYTHON_INFO],
147 capture_output=True, check=True)
148
149 arch, version = res.stdout.decode('utf-8').split(':')
150
151 version = distutils.version.LooseVersion(version)
152
153 return {
154 'arch': arch,
155 'version': version,
156 'py3': version >= distutils.version.LooseVersion('3'),
157 }
1 # util.py - Common packaging utility code.
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import distutils.version
11 import getpass
12 import os
13 import pathlib
14 import subprocess
15 import tarfile
16 import zipfile
17
18
19 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
20 with tarfile.open(source, 'r') as tf:
21 tf.extractall(dest)
22
23
24 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
25 with zipfile.ZipFile(source, 'r') as zf:
26 zf.extractall(dest)
27
28
29 def find_vc_runtime_files(x64=False):
30 """Finds Visual C++ Runtime DLLs to include in distribution."""
31 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
32
33 prefix = 'amd64' if x64 else 'x86'
34
35 candidates = sorted(p for p in os.listdir(winsxs)
36 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix))
37
38 for p in candidates:
39 print('found candidate VC runtime: %s' % p)
40
41 # Take the newest version.
42 version = candidates[-1]
43
44 d = winsxs / version
45
46 return [
47 d / 'msvcm90.dll',
48 d / 'msvcp90.dll',
49 d / 'msvcr90.dll',
50 winsxs / 'Manifests' / ('%s.manifest' % version),
51 ]
52
53
54 def windows_10_sdk_info():
55 """Resolves information about the Windows 10 SDK."""
56
57 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
58
59 if not base.is_dir():
60 raise Exception('unable to find Windows 10 SDK at %s' % base)
61
62 # Find the latest version.
63 bin_base = base / 'bin'
64
65 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
66 version = sorted(versions, reverse=True)[0]
67
68 bin_version = bin_base / version
69
70 return {
71 'root': base,
72 'version': version,
73 'bin_root': bin_version,
74 'bin_x86': bin_version / 'x86',
75 'bin_x64': bin_version / 'x64'
76 }
77
78
79 def find_signtool():
80 """Find signtool.exe from the Windows SDK."""
81 sdk = windows_10_sdk_info()
82
83 for key in ('bin_x64', 'bin_x86'):
84 p = sdk[key] / 'signtool.exe'
85
86 if p.exists():
87 return p
88
89 raise Exception('could not find signtool.exe in Windows 10 SDK')
90
91
92 def sign_with_signtool(file_path, description, subject_name=None,
93 cert_path=None, cert_password=None,
94 timestamp_url=None):
95 """Digitally sign a file with signtool.exe.
96
97 ``file_path`` is file to sign.
98 ``description`` is text that goes in the signature.
99
100 The signing certificate can be specified by ``cert_path`` or
101 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
102 to signtool.exe, respectively.
103
104 The certificate password can be specified via ``cert_password``. If
105 not provided, you will be prompted for the password.
106
107 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
108 argument to signtool.exe).
109 """
110 if cert_path and subject_name:
111 raise ValueError('cannot specify both cert_path and subject_name')
112
113 while cert_path and not cert_password:
114 cert_password = getpass.getpass('password for %s: ' % cert_path)
115
116 args = [
117 str(find_signtool()), 'sign',
118 '/v',
119 '/fd', 'sha256',
120 '/d', description,
121 ]
122
123 if cert_path:
124 args.extend(['/f', str(cert_path), '/p', cert_password])
125 elif subject_name:
126 args.extend(['/n', subject_name])
127
128 if timestamp_url:
129 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
130
131 args.append(str(file_path))
132
133 print('signing %s' % file_path)
134 subprocess.run(args, check=True)
135
136
137 PRINT_PYTHON_INFO = '''
138 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
139 '''.strip()
140
141
142 def python_exe_info(python_exe: pathlib.Path):
143 """Obtain information about a Python executable."""
144
145 res = subprocess.run(
146 [str(python_exe), '-c', PRINT_PYTHON_INFO],
147 capture_output=True, check=True)
148
149 arch, version = res.stdout.decode('utf-8').split(':')
150
151 version = distutils.version.LooseVersion(version)
152
153 return {
154 'arch': arch,
155 'version': version,
156 'py3': version >= distutils.version.LooseVersion('3'),
157 }
@@ -1,239 +1,239
1 # wix.py - WiX installer functionality
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import os
11 import pathlib
12 import re
13 import subprocess
14
15 from .downloads import (
16 download_entry,
17 )
18 from .py2exe import (
19 build_py2exe,
20 )
21 from .util import (
22 extract_zip_to_directory,
23 sign_with_signtool,
24 )
25
26
27 SUPPORT_WXS = [
28 ('contrib.wxs', r'contrib'),
29 ('dist.wxs', r'dist'),
30 ('doc.wxs', r'doc'),
31 ('help.wxs', r'mercurial\help'),
32 ('i18n.wxs', r'i18n'),
33 ('locale.wxs', r'mercurial\locale'),
34 ('templates.wxs', r'mercurial\templates'),
35 ]
36
37
38 EXTRA_PACKAGES = {
39 'distutils',
40 'pygments',
41 }
42
43
44 def find_version(source_dir: pathlib.Path):
45 version_py = source_dir / 'mercurial' / '__version__.py'
46
47 with version_py.open('r', encoding='utf-8') as fh:
48 source = fh.read().strip()
49
50 m = re.search('version = b"(.*)"', source)
51 return m.group(1)
52
53
54 def normalize_version(version):
55 """Normalize Mercurial version string so WiX accepts it.
56
57 Version strings have to be numeric X.Y.Z.
58 """
59
60 if '+' in version:
61 version, extra = version.split('+', 1)
62 else:
63 extra = None
64
65 # 4.9rc0
66 if version[:-1].endswith('rc'):
67 version = version[:-3]
68
69 versions = [int(v) for v in version.split('.')]
70 while len(versions) < 3:
71 versions.append(0)
72
73 major, minor, build = versions[:3]
74
75 if extra:
76 # <commit count>-<hash>+<date>
77 build = int(extra.split('-')[0])
78
79 return '.'.join('%d' % x for x in (major, minor, build))
80
81
82 def ensure_vc90_merge_modules(build_dir):
83 x86 = (
84 download_entry('vc9-crt-x86-msm', build_dir,
85 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
86 download_entry('vc9-crt-x86-msm-policy', build_dir,
87 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
88 )
89
90 x64 = (
91 download_entry('vc9-crt-x64-msm', build_dir,
92 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
93 download_entry('vc9-crt-x64-msm-policy', build_dir,
94 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
95 )
96 return {
97 'x86': x86,
98 'x64': x64,
99 }
100
101
102 def run_candle(wix, cwd, wxs, source_dir, defines=None):
103 args = [
104 str(wix / 'candle.exe'),
105 '-nologo',
106 str(wxs),
107 '-dSourceDir=%s' % source_dir,
108 ]
109
110 if defines:
111 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
112
113 subprocess.run(args, cwd=str(cwd), check=True)
114
115
116 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
117 cert_password=None, timestamp_url=None):
118 """Create a callable that will use signtool to sign hg.exe."""
119
120 def post_build_sign(source_dir, build_dir, dist_dir, version):
121 description = '%s %s' % (name, version)
122
123 sign_with_signtool(dist_dir / 'hg.exe', description,
124 subject_name=subject_name, cert_path=cert_path,
125 cert_password=cert_password,
126 timestamp_url=timestamp_url)
127
128 return post_build_sign
129
130
131 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
132 msi_name='mercurial', version=None, post_build_fn=None):
133 """Build a WiX MSI installer.
134
135 ``source_dir`` is the path to the Mercurial source tree to use.
136 ``arch`` is the target architecture. either ``x86`` or ``x64``.
137 ``python_exe`` is the path to the Python executable to use/bundle.
138 ``version`` is the Mercurial version string. If not defined,
139 ``mercurial/__version__.py`` will be consulted.
140 ``post_build_fn`` is a callable that will be called after building
141 Mercurial but before invoking WiX. It can be used to e.g. facilitate
142 signing. It is passed the paths to the Mercurial source, build, and
143 dist directories and the resolved Mercurial version.
144 """
145 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
146
147 hg_build_dir = source_dir / 'build'
148 dist_dir = source_dir / 'dist'
149
150 requirements_txt = (source_dir / 'contrib' / 'packaging' /
151 'wix' / 'requirements.txt')
152
153 build_py2exe(source_dir, hg_build_dir,
154 python_exe, 'wix', requirements_txt,
155 extra_packages=EXTRA_PACKAGES)
156
157 version = version or normalize_version(find_version(source_dir))
158 print('using version string: %s' % version)
159
160 if post_build_fn:
161 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
162
163 build_dir = hg_build_dir / ('wix-%s' % arch)
164
165 build_dir.mkdir(exist_ok=True)
166
167 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
168 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
169
170 if not wix_path.exists():
171 extract_zip_to_directory(wix_pkg, wix_path)
172
173 ensure_vc90_merge_modules(hg_build_dir)
174
175 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
176
177 defines = {'Platform': arch}
178
179 for wxs, rel_path in SUPPORT_WXS:
180 wxs = source_dir / 'contrib' / 'packaging' / 'wix' / wxs
181 wxs_source_dir = source_dir / rel_path
182 run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
183
184 source = source_dir / 'contrib' / 'packaging' / 'wix' / 'mercurial.wxs'
185 defines['Version'] = version
186 defines['Comments'] = 'Installs Mercurial version %s' % version
187 defines['VCRedistSrcDir'] = str(hg_build_dir)
188
189 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
190
191 msi_path = source_dir / 'dist' / (
192 '%s-%s-%s.msi' % (msi_name, version, arch))
193
194 args = [
195 str(wix_path / 'light.exe'),
196 '-nologo',
197 '-ext', 'WixUIExtension',
198 '-sw1076',
199 '-spdb',
200 '-o', str(msi_path),
201 ]
202
203 for source, rel_path in SUPPORT_WXS:
204 assert source.endswith('.wxs')
205 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
206
207 args.append(str(build_dir / 'mercurial.wixobj'))
208
209 subprocess.run(args, cwd=str(source_dir), check=True)
210
211 print('%s created' % msi_path)
212
213 return {
214 'msi_path': msi_path,
215 }
216
217
218 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
219 name: str, version=None, subject_name=None,
220 cert_path=None, cert_password=None,
221 timestamp_url=None):
222 """Build an installer with signed executables."""
223
224 post_build_fn = make_post_build_signing_fn(
225 name,
226 subject_name=subject_name,
227 cert_path=cert_path,
228 cert_password=cert_password,
229 timestamp_url=timestamp_url)
230
231 info = build_installer(source_dir, python_exe=python_exe,
232 msi_name=name.lower(), version=version,
233 post_build_fn=post_build_fn)
234
235 description = '%s %s' % (name, version)
236
237 sign_with_signtool(info['msi_path'], description,
238 subject_name=subject_name, cert_path=cert_path,
239 cert_password=cert_password, timestamp_url=timestamp_url)
1 # wix.py - WiX installer functionality
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import os
11 import pathlib
12 import re
13 import subprocess
14
15 from .downloads import (
16 download_entry,
17 )
18 from .py2exe import (
19 build_py2exe,
20 )
21 from .util import (
22 extract_zip_to_directory,
23 sign_with_signtool,
24 )
25
26
27 SUPPORT_WXS = [
28 ('contrib.wxs', r'contrib'),
29 ('dist.wxs', r'dist'),
30 ('doc.wxs', r'doc'),
31 ('help.wxs', r'mercurial\help'),
32 ('i18n.wxs', r'i18n'),
33 ('locale.wxs', r'mercurial\locale'),
34 ('templates.wxs', r'mercurial\templates'),
35 ]
36
37
38 EXTRA_PACKAGES = {
39 'distutils',
40 'pygments',
41 }
42
43
44 def find_version(source_dir: pathlib.Path):
45 version_py = source_dir / 'mercurial' / '__version__.py'
46
47 with version_py.open('r', encoding='utf-8') as fh:
48 source = fh.read().strip()
49
50 m = re.search('version = b"(.*)"', source)
51 return m.group(1)
52
53
54 def normalize_version(version):
55 """Normalize Mercurial version string so WiX accepts it.
56
57 Version strings have to be numeric X.Y.Z.
58 """
59
60 if '+' in version:
61 version, extra = version.split('+', 1)
62 else:
63 extra = None
64
65 # 4.9rc0
66 if version[:-1].endswith('rc'):
67 version = version[:-3]
68
69 versions = [int(v) for v in version.split('.')]
70 while len(versions) < 3:
71 versions.append(0)
72
73 major, minor, build = versions[:3]
74
75 if extra:
76 # <commit count>-<hash>+<date>
77 build = int(extra.split('-')[0])
78
79 return '.'.join('%d' % x for x in (major, minor, build))
80
81
82 def ensure_vc90_merge_modules(build_dir):
83 x86 = (
84 download_entry('vc9-crt-x86-msm', build_dir,
85 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
86 download_entry('vc9-crt-x86-msm-policy', build_dir,
87 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
88 )
89
90 x64 = (
91 download_entry('vc9-crt-x64-msm', build_dir,
92 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
93 download_entry('vc9-crt-x64-msm-policy', build_dir,
94 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
95 )
96 return {
97 'x86': x86,
98 'x64': x64,
99 }
100
101
102 def run_candle(wix, cwd, wxs, source_dir, defines=None):
103 args = [
104 str(wix / 'candle.exe'),
105 '-nologo',
106 str(wxs),
107 '-dSourceDir=%s' % source_dir,
108 ]
109
110 if defines:
111 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
112
113 subprocess.run(args, cwd=str(cwd), check=True)
114
115
116 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
117 cert_password=None, timestamp_url=None):
118 """Create a callable that will use signtool to sign hg.exe."""
119
120 def post_build_sign(source_dir, build_dir, dist_dir, version):
121 description = '%s %s' % (name, version)
122
123 sign_with_signtool(dist_dir / 'hg.exe', description,
124 subject_name=subject_name, cert_path=cert_path,
125 cert_password=cert_password,
126 timestamp_url=timestamp_url)
127
128 return post_build_sign
129
130
131 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
132 msi_name='mercurial', version=None, post_build_fn=None):
133 """Build a WiX MSI installer.
134
135 ``source_dir`` is the path to the Mercurial source tree to use.
136 ``arch`` is the target architecture. either ``x86`` or ``x64``.
137 ``python_exe`` is the path to the Python executable to use/bundle.
138 ``version`` is the Mercurial version string. If not defined,
139 ``mercurial/__version__.py`` will be consulted.
140 ``post_build_fn`` is a callable that will be called after building
141 Mercurial but before invoking WiX. It can be used to e.g. facilitate
142 signing. It is passed the paths to the Mercurial source, build, and
143 dist directories and the resolved Mercurial version.
144 """
145 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
146
147 hg_build_dir = source_dir / 'build'
148 dist_dir = source_dir / 'dist'
149
150 requirements_txt = (source_dir / 'contrib' / 'packaging' /
151 'wix' / 'requirements.txt')
152
153 build_py2exe(source_dir, hg_build_dir,
154 python_exe, 'wix', requirements_txt,
155 extra_packages=EXTRA_PACKAGES)
156
157 version = version or normalize_version(find_version(source_dir))
158 print('using version string: %s' % version)
159
160 if post_build_fn:
161 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
162
163 build_dir = hg_build_dir / ('wix-%s' % arch)
164
165 build_dir.mkdir(exist_ok=True)
166
167 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
168 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
169
170 if not wix_path.exists():
171 extract_zip_to_directory(wix_pkg, wix_path)
172
173 ensure_vc90_merge_modules(hg_build_dir)
174
175 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
176
177 defines = {'Platform': arch}
178
179 for wxs, rel_path in SUPPORT_WXS:
180 wxs = source_dir / 'contrib' / 'packaging' / 'wix' / wxs
181 wxs_source_dir = source_dir / rel_path
182 run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
183
184 source = source_dir / 'contrib' / 'packaging' / 'wix' / 'mercurial.wxs'
185 defines['Version'] = version
186 defines['Comments'] = 'Installs Mercurial version %s' % version
187 defines['VCRedistSrcDir'] = str(hg_build_dir)
188
189 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
190
191 msi_path = source_dir / 'dist' / (
192 '%s-%s-%s.msi' % (msi_name, version, arch))
193
194 args = [
195 str(wix_path / 'light.exe'),
196 '-nologo',
197 '-ext', 'WixUIExtension',
198 '-sw1076',
199 '-spdb',
200 '-o', str(msi_path),
201 ]
202
203 for source, rel_path in SUPPORT_WXS:
204 assert source.endswith('.wxs')
205 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
206
207 args.append(str(build_dir / 'mercurial.wixobj'))
208
209 subprocess.run(args, cwd=str(source_dir), check=True)
210
211 print('%s created' % msi_path)
212
213 return {
214 'msi_path': msi_path,
215 }
216
217
218 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
219 name: str, version=None, subject_name=None,
220 cert_path=None, cert_password=None,
221 timestamp_url=None):
222 """Build an installer with signed executables."""
223
224 post_build_fn = make_post_build_signing_fn(
225 name,
226 subject_name=subject_name,
227 cert_path=cert_path,
228 cert_password=cert_password,
229 timestamp_url=timestamp_url)
230
231 info = build_installer(source_dir, python_exe=python_exe,
232 msi_name=name.lower(), version=version,
233 post_build_fn=post_build_fn)
234
235 description = '%s %s' % (name, version)
236
237 sign_with_signtool(info['msi_path'], description,
238 subject_name=subject_name, cert_path=cert_path,
239 cert_password=cert_password, timestamp_url=timestamp_url)
General Comments 0
You need to be logged in to leave comments. Login now