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