##// END OF EJS Templates
packaging: remove py2exe / Python 2.7 support...
Gregory Szorc -
r49703:17d5e25b default
parent child Browse files
Show More
@@ -1,195 +1,154 b''
1 # cli.py - Command line interface for automation
1 # cli.py - Command line interface for automation
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 argparse
10 import argparse
11 import os
11 import os
12 import pathlib
12 import pathlib
13
13
14 from . import (
14 from . import (
15 inno,
15 inno,
16 wix,
16 wix,
17 )
17 )
18
18
19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
20 SOURCE_DIR = HERE.parent.parent.parent
20 SOURCE_DIR = HERE.parent.parent.parent
21
21
22
22
23 def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
23 def build_inno(pyoxidizer_target, iscc=None, version=None):
24 if not pyoxidizer_target and not python:
25 raise Exception("--python required unless building with PyOxidizer")
26
27 if python and not os.path.isabs(python):
28 raise Exception("--python arg must be an absolute path")
29
30 if iscc:
24 if iscc:
31 iscc = pathlib.Path(iscc)
25 iscc = pathlib.Path(iscc)
32 else:
26 else:
33 iscc = (
27 iscc = (
34 pathlib.Path(os.environ["ProgramFiles(x86)"])
28 pathlib.Path(os.environ["ProgramFiles(x86)"])
35 / "Inno Setup 5"
29 / "Inno Setup 5"
36 / "ISCC.exe"
30 / "ISCC.exe"
37 )
31 )
38
32
39 build_dir = SOURCE_DIR / "build"
33 build_dir = SOURCE_DIR / "build"
40
34
41 if pyoxidizer_target:
35 inno.build_with_pyoxidizer(
42 inno.build_with_pyoxidizer(
36 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
37 )
44 )
45 else:
46 inno.build_with_py2exe(
47 SOURCE_DIR,
48 build_dir,
49 pathlib.Path(python),
50 iscc,
51 version=version,
52 )
53
38
54
39
55 def build_wix(
40 def build_wix(
41 pyoxidizer_target,
56 name=None,
42 name=None,
57 pyoxidizer_target=None,
58 python=None,
59 version=None,
43 version=None,
60 sign_sn=None,
44 sign_sn=None,
61 sign_cert=None,
45 sign_cert=None,
62 sign_password=None,
46 sign_password=None,
63 sign_timestamp_url=None,
47 sign_timestamp_url=None,
64 extra_packages_script=None,
65 extra_wxs=None,
48 extra_wxs=None,
66 extra_features=None,
49 extra_features=None,
67 extra_pyoxidizer_vars=None,
50 extra_pyoxidizer_vars=None,
68 ):
51 ):
69 if not pyoxidizer_target and not python:
70 raise Exception("--python required unless building with PyOxidizer")
71
72 if python and not os.path.isabs(python):
73 raise Exception("--python arg must be an absolute path")
74
75 kwargs = {
52 kwargs = {
76 "source_dir": SOURCE_DIR,
53 "source_dir": SOURCE_DIR,
77 "version": version,
54 "version": version,
55 "target_triple": pyoxidizer_target,
56 "extra_pyoxidizer_vars": extra_pyoxidizer_vars,
78 }
57 }
79
58
80 if pyoxidizer_target:
81 fn = wix.build_installer_pyoxidizer
82 kwargs["target_triple"] = pyoxidizer_target
83 kwargs["extra_pyoxidizer_vars"] = extra_pyoxidizer_vars
84 else:
85 fn = wix.build_installer_py2exe
86 kwargs["python_exe"] = pathlib.Path(python)
87
88 if extra_packages_script:
89 if pyoxidizer_target:
90 raise Exception(
91 "pyoxidizer does not support --extra-packages-script"
92 )
93 kwargs["extra_packages_script"] = extra_packages_script
94 if extra_wxs:
59 if extra_wxs:
95 kwargs["extra_wxs"] = dict(
60 kwargs["extra_wxs"] = dict(
96 thing.split("=") for thing in extra_wxs.split(",")
61 thing.split("=") for thing in extra_wxs.split(",")
97 )
62 )
98 if extra_features:
63 if extra_features:
99 kwargs["extra_features"] = extra_features.split(",")
64 kwargs["extra_features"] = extra_features.split(",")
100
65
101 if sign_sn or sign_cert:
66 if sign_sn or sign_cert:
102 kwargs["signing_info"] = {
67 kwargs["signing_info"] = {
103 "name": name,
68 "name": name,
104 "subject_name": sign_sn,
69 "subject_name": sign_sn,
105 "cert_path": sign_cert,
70 "cert_path": sign_cert,
106 "cert_password": sign_password,
71 "cert_password": sign_password,
107 "timestamp_url": sign_timestamp_url,
72 "timestamp_url": sign_timestamp_url,
108 }
73 }
109
74
110 fn(**kwargs)
75 wix.build_installer_pyoxidizer(**kwargs)
111
76
112
77
113 def get_parser():
78 def get_parser():
114 parser = argparse.ArgumentParser()
79 parser = argparse.ArgumentParser()
115
80
116 subparsers = parser.add_subparsers()
81 subparsers = parser.add_subparsers()
117
82
118 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
83 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
119 sp.add_argument(
84 sp.add_argument(
120 "--pyoxidizer-target",
85 "--pyoxidizer-target",
121 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
86 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
87 required=True,
122 help="Build with PyOxidizer targeting this host triple",
88 help="Build with PyOxidizer targeting this host triple",
123 )
89 )
124 sp.add_argument("--python", help="path to python.exe to use")
125 sp.add_argument("--iscc", help="path to iscc.exe to use")
90 sp.add_argument("--iscc", help="path to iscc.exe to use")
126 sp.add_argument(
91 sp.add_argument(
127 "--version",
92 "--version",
128 help="Mercurial version string to use "
93 help="Mercurial version string to use "
129 "(detected from __version__.py if not defined",
94 "(detected from __version__.py if not defined",
130 )
95 )
131 sp.set_defaults(func=build_inno)
96 sp.set_defaults(func=build_inno)
132
97
133 sp = subparsers.add_parser(
98 sp = subparsers.add_parser(
134 "wix", help="Build Windows installer with WiX Toolset"
99 "wix", help="Build Windows installer with WiX Toolset"
135 )
100 )
136 sp.add_argument("--name", help="Application name", default="Mercurial")
101 sp.add_argument("--name", help="Application name", default="Mercurial")
137 sp.add_argument(
102 sp.add_argument(
138 "--pyoxidizer-target",
103 "--pyoxidizer-target",
139 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
104 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
105 required=True,
140 help="Build with PyOxidizer targeting this host triple",
106 help="Build with PyOxidizer targeting this host triple",
141 )
107 )
142 sp.add_argument("--python", help="Path to Python executable to use")
143 sp.add_argument(
108 sp.add_argument(
144 "--sign-sn",
109 "--sign-sn",
145 help="Subject name (or fragment thereof) of certificate "
110 help="Subject name (or fragment thereof) of certificate "
146 "to use for signing",
111 "to use for signing",
147 )
112 )
148 sp.add_argument(
113 sp.add_argument(
149 "--sign-cert", help="Path to certificate to use for signing"
114 "--sign-cert", help="Path to certificate to use for signing"
150 )
115 )
151 sp.add_argument("--sign-password", help="Password for signing certificate")
116 sp.add_argument("--sign-password", help="Password for signing certificate")
152 sp.add_argument(
117 sp.add_argument(
153 "--sign-timestamp-url",
118 "--sign-timestamp-url",
154 help="URL of timestamp server to use for signing",
119 help="URL of timestamp server to use for signing",
155 )
120 )
156 sp.add_argument("--version", help="Version string to use")
121 sp.add_argument("--version", help="Version string to use")
157 sp.add_argument(
122 sp.add_argument(
158 "--extra-packages-script",
159 help=(
160 "Script to execute to include extra packages in " "py2exe binary."
161 ),
162 )
163 sp.add_argument(
164 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
123 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
165 )
124 )
166 sp.add_argument(
125 sp.add_argument(
167 "--extra-features",
126 "--extra-features",
168 help=(
127 help=(
169 "CSV of extra feature names to include "
128 "CSV of extra feature names to include "
170 "in the installer from the extra wxs files"
129 "in the installer from the extra wxs files"
171 ),
130 ),
172 )
131 )
173
132
174 sp.add_argument(
133 sp.add_argument(
175 "--extra-pyoxidizer-vars",
134 "--extra-pyoxidizer-vars",
176 help="json map of extra variables to pass to pyoxidizer",
135 help="json map of extra variables to pass to pyoxidizer",
177 )
136 )
178
137
179 sp.set_defaults(func=build_wix)
138 sp.set_defaults(func=build_wix)
180
139
181 return parser
140 return parser
182
141
183
142
184 def main():
143 def main():
185 parser = get_parser()
144 parser = get_parser()
186 args = parser.parse_args()
145 args = parser.parse_args()
187
146
188 if not hasattr(args, "func"):
147 if not hasattr(args, "func"):
189 parser.print_help()
148 parser.print_help()
190 return
149 return
191
150
192 kwargs = dict(vars(args))
151 kwargs = dict(vars(args))
193 del kwargs["func"]
152 del kwargs["func"]
194
153
195 args.func(**kwargs)
154 args.func(**kwargs)
@@ -1,182 +1,140 b''
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': {
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 }
28 }
71
29
72
30
73 def hash_path(p: pathlib.Path):
31 def hash_path(p: pathlib.Path):
74 h = hashlib.sha256()
32 h = hashlib.sha256()
75
33
76 with p.open('rb') as fh:
34 with p.open('rb') as fh:
77 while True:
35 while True:
78 chunk = fh.read(65536)
36 chunk = fh.read(65536)
79 if not chunk:
37 if not chunk:
80 break
38 break
81
39
82 h.update(chunk)
40 h.update(chunk)
83
41
84 return h.hexdigest()
42 return h.hexdigest()
85
43
86
44
87 class IntegrityError(Exception):
45 class IntegrityError(Exception):
88 """Represents an integrity error when downloading a URL."""
46 """Represents an integrity error when downloading a URL."""
89
47
90
48
91 def secure_download_stream(url, size, sha256):
49 def secure_download_stream(url, size, sha256):
92 """Securely download a URL to a stream of chunks.
50 """Securely download a URL to a stream of chunks.
93
51
94 If the integrity of the download fails, an IntegrityError is
52 If the integrity of the download fails, an IntegrityError is
95 raised.
53 raised.
96 """
54 """
97 h = hashlib.sha256()
55 h = hashlib.sha256()
98 length = 0
56 length = 0
99
57
100 with urllib.request.urlopen(url) as fh:
58 with urllib.request.urlopen(url) as fh:
101 if (
59 if (
102 not url.endswith('.gz')
60 not url.endswith('.gz')
103 and fh.info().get('Content-Encoding') == 'gzip'
61 and fh.info().get('Content-Encoding') == 'gzip'
104 ):
62 ):
105 fh = gzip.GzipFile(fileobj=fh)
63 fh = gzip.GzipFile(fileobj=fh)
106
64
107 while True:
65 while True:
108 chunk = fh.read(65536)
66 chunk = fh.read(65536)
109 if not chunk:
67 if not chunk:
110 break
68 break
111
69
112 h.update(chunk)
70 h.update(chunk)
113 length += len(chunk)
71 length += len(chunk)
114
72
115 yield chunk
73 yield chunk
116
74
117 digest = h.hexdigest()
75 digest = h.hexdigest()
118
76
119 if length != size:
77 if length != size:
120 raise IntegrityError(
78 raise IntegrityError(
121 'size mismatch on %s: wanted %d; got %d' % (url, size, length)
79 'size mismatch on %s: wanted %d; got %d' % (url, size, length)
122 )
80 )
123
81
124 if digest != sha256:
82 if digest != sha256:
125 raise IntegrityError(
83 raise IntegrityError(
126 'sha256 mismatch on %s: wanted %s; got %s' % (url, sha256, digest)
84 'sha256 mismatch on %s: wanted %s; got %s' % (url, sha256, digest)
127 )
85 )
128
86
129
87
130 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
88 def download_to_path(url: str, path: pathlib.Path, size: int, sha256: str):
131 """Download a URL to a filesystem path, possibly with verification."""
89 """Download a URL to a filesystem path, possibly with verification."""
132
90
133 # We download to a temporary file and rename at the end so there's
91 # We download to a temporary file and rename at the end so there's
134 # no chance of the final file being partially written or containing
92 # no chance of the final file being partially written or containing
135 # bad data.
93 # bad data.
136 print('downloading %s to %s' % (url, path))
94 print('downloading %s to %s' % (url, path))
137
95
138 if path.exists():
96 if path.exists():
139 good = True
97 good = True
140
98
141 if path.stat().st_size != size:
99 if path.stat().st_size != size:
142 print('existing file size is wrong; removing')
100 print('existing file size is wrong; removing')
143 good = False
101 good = False
144
102
145 if good:
103 if good:
146 if hash_path(path) != sha256:
104 if hash_path(path) != sha256:
147 print('existing file hash is wrong; removing')
105 print('existing file hash is wrong; removing')
148 good = False
106 good = False
149
107
150 if good:
108 if good:
151 print('%s exists and passes integrity checks' % path)
109 print('%s exists and passes integrity checks' % path)
152 return
110 return
153
111
154 path.unlink()
112 path.unlink()
155
113
156 tmp = path.with_name('%s.tmp' % path.name)
114 tmp = path.with_name('%s.tmp' % path.name)
157
115
158 try:
116 try:
159 with tmp.open('wb') as fh:
117 with tmp.open('wb') as fh:
160 for chunk in secure_download_stream(url, size, sha256):
118 for chunk in secure_download_stream(url, size, sha256):
161 fh.write(chunk)
119 fh.write(chunk)
162 except IntegrityError:
120 except IntegrityError:
163 tmp.unlink()
121 tmp.unlink()
164 raise
122 raise
165
123
166 tmp.rename(path)
124 tmp.rename(path)
167 print('successfully downloaded %s' % url)
125 print('successfully downloaded %s' % url)
168
126
169
127
170 def download_entry(
128 def download_entry(
171 name: dict, dest_path: pathlib.Path, local_name=None
129 name: dict, dest_path: pathlib.Path, local_name=None
172 ) -> pathlib.Path:
130 ) -> pathlib.Path:
173 entry = DOWNLOADS[name]
131 entry = DOWNLOADS[name]
174
132
175 url = entry['url']
133 url = entry['url']
176
134
177 local_name = local_name or url[url.rindex('/') + 1 :]
135 local_name = local_name or url[url.rindex('/') + 1 :]
178
136
179 local_path = dest_path / local_name
137 local_path = dest_path / local_name
180 download_to_path(url, local_path, entry['size'], entry['sha256'])
138 download_to_path(url, local_path, entry['size'], entry['sha256'])
181
139
182 return local_path, entry
140 return local_path, entry
@@ -1,244 +1,154 b''
1 # inno.py - Inno Setup functionality.
1 # inno.py - Inno Setup 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 shutil
12 import shutil
13 import subprocess
13 import subprocess
14
14
15 import jinja2
15 import jinja2
16
16
17 from .py2exe import (
18 build_py2exe,
19 stage_install,
20 )
21 from .pyoxidizer import create_pyoxidizer_install_layout
17 from .pyoxidizer import create_pyoxidizer_install_layout
22 from .util import (
18 from .util import (
23 find_legacy_vc_runtime_files,
24 normalize_windows_version,
19 normalize_windows_version,
25 process_install_rules,
20 process_install_rules,
26 read_version_py,
21 read_version_py,
27 )
22 )
28
23
29 EXTRA_PACKAGES = {
30 'dulwich',
31 'keyring',
32 'pygments',
33 'win32ctypes',
34 }
35
36 EXTRA_INCLUDES = {
37 '_curses',
38 '_curses_panel',
39 }
40
24
41 EXTRA_INSTALL_RULES = [
25 EXTRA_INSTALL_RULES = [
42 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
26 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
43 ]
27 ]
44
28
45 PACKAGE_FILES_METADATA = {
29 PACKAGE_FILES_METADATA = {
46 'ReadMe.html': 'Flags: isreadme',
30 'ReadMe.html': 'Flags: isreadme',
47 }
31 }
48
32
49
33
50 def build_with_py2exe(
51 source_dir: pathlib.Path,
52 build_dir: pathlib.Path,
53 python_exe: pathlib.Path,
54 iscc_exe: pathlib.Path,
55 version=None,
56 ):
57 """Build the Inno installer using py2exe.
58
59 Build files will be placed in ``build_dir``.
60
61 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
62 for finding the Python 2.7 toolchain. So, we require the environment
63 to already be configured with an active toolchain.
64 """
65 if not iscc_exe.exists():
66 raise Exception('%s does not exist' % iscc_exe)
67
68 vc_x64 = r'\x64' in os.environ.get('LIB', '')
69 arch = 'x64' if vc_x64 else 'x86'
70 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
71 staging_dir = inno_build_dir / 'stage'
72
73 requirements_txt = (
74 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
75 )
76
77 inno_build_dir.mkdir(parents=True, exist_ok=True)
78
79 build_py2exe(
80 source_dir,
81 build_dir,
82 python_exe,
83 'inno',
84 requirements_txt,
85 extra_packages=EXTRA_PACKAGES,
86 extra_includes=EXTRA_INCLUDES,
87 )
88
89 # Purge the staging directory for every build so packaging is
90 # pristine.
91 if staging_dir.exists():
92 print('purging %s' % staging_dir)
93 shutil.rmtree(staging_dir)
94
95 # Now assemble all the packaged files into the staging directory.
96 stage_install(source_dir, staging_dir)
97
98 # We also install some extra files.
99 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
100
101 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
102 for f in find_legacy_vc_runtime_files(vc_x64):
103 if f.name.endswith('.manifest'):
104 basename = 'Microsoft.VC90.CRT.manifest'
105 else:
106 basename = f.name
107
108 dest_path = staging_dir / basename
109
110 print('copying %s to %s' % (f, dest_path))
111 shutil.copyfile(f, dest_path)
112
113 build_installer(
114 source_dir,
115 inno_build_dir,
116 staging_dir,
117 iscc_exe,
118 version,
119 arch="x64" if vc_x64 else None,
120 suffix="-python2",
121 )
122
123
124 def build_with_pyoxidizer(
34 def build_with_pyoxidizer(
125 source_dir: pathlib.Path,
35 source_dir: pathlib.Path,
126 build_dir: pathlib.Path,
36 build_dir: pathlib.Path,
127 target_triple: str,
37 target_triple: str,
128 iscc_exe: pathlib.Path,
38 iscc_exe: pathlib.Path,
129 version=None,
39 version=None,
130 ):
40 ):
131 """Build the Inno installer using PyOxidizer."""
41 """Build the Inno installer using PyOxidizer."""
132 if not iscc_exe.exists():
42 if not iscc_exe.exists():
133 raise Exception("%s does not exist" % iscc_exe)
43 raise Exception("%s does not exist" % iscc_exe)
134
44
135 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
45 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
136 staging_dir = inno_build_dir / "stage"
46 staging_dir = inno_build_dir / "stage"
137
47
138 inno_build_dir.mkdir(parents=True, exist_ok=True)
48 inno_build_dir.mkdir(parents=True, exist_ok=True)
139 create_pyoxidizer_install_layout(
49 create_pyoxidizer_install_layout(
140 source_dir, inno_build_dir, staging_dir, target_triple
50 source_dir, inno_build_dir, staging_dir, target_triple
141 )
51 )
142
52
143 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
53 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
144
54
145 build_installer(
55 build_installer(
146 source_dir,
56 source_dir,
147 inno_build_dir,
57 inno_build_dir,
148 staging_dir,
58 staging_dir,
149 iscc_exe,
59 iscc_exe,
150 version,
60 version,
151 arch="x64" if "x86_64" in target_triple else None,
61 arch="x64" if "x86_64" in target_triple else None,
152 )
62 )
153
63
154
64
155 def build_installer(
65 def build_installer(
156 source_dir: pathlib.Path,
66 source_dir: pathlib.Path,
157 inno_build_dir: pathlib.Path,
67 inno_build_dir: pathlib.Path,
158 staging_dir: pathlib.Path,
68 staging_dir: pathlib.Path,
159 iscc_exe: pathlib.Path,
69 iscc_exe: pathlib.Path,
160 version,
70 version,
161 arch=None,
71 arch=None,
162 suffix="",
72 suffix="",
163 ):
73 ):
164 """Build an Inno installer from staged Mercurial files.
74 """Build an Inno installer from staged Mercurial files.
165
75
166 This function is agnostic about how to build Mercurial. It just
76 This function is agnostic about how to build Mercurial. It just
167 cares that Mercurial files are in ``staging_dir``.
77 cares that Mercurial files are in ``staging_dir``.
168 """
78 """
169 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
79 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
170
80
171 # The final package layout is simply a mirror of the staging directory.
81 # The final package layout is simply a mirror of the staging directory.
172 package_files = []
82 package_files = []
173 for root, dirs, files in os.walk(staging_dir):
83 for root, dirs, files in os.walk(staging_dir):
174 dirs.sort()
84 dirs.sort()
175
85
176 root = pathlib.Path(root)
86 root = pathlib.Path(root)
177
87
178 for f in sorted(files):
88 for f in sorted(files):
179 full = root / f
89 full = root / f
180 rel = full.relative_to(staging_dir)
90 rel = full.relative_to(staging_dir)
181 if str(rel.parent) == '.':
91 if str(rel.parent) == '.':
182 dest_dir = '{app}'
92 dest_dir = '{app}'
183 else:
93 else:
184 dest_dir = '{app}\\%s' % rel.parent
94 dest_dir = '{app}\\%s' % rel.parent
185
95
186 package_files.append(
96 package_files.append(
187 {
97 {
188 'source': rel,
98 'source': rel,
189 'dest_dir': dest_dir,
99 'dest_dir': dest_dir,
190 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
100 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
191 }
101 }
192 )
102 )
193
103
194 print('creating installer')
104 print('creating installer')
195
105
196 # Install Inno files by rendering a template.
106 # Install Inno files by rendering a template.
197 jinja_env = jinja2.Environment(
107 jinja_env = jinja2.Environment(
198 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
108 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
199 # Need to change these to prevent conflict with Inno Setup.
109 # Need to change these to prevent conflict with Inno Setup.
200 comment_start_string='{##',
110 comment_start_string='{##',
201 comment_end_string='##}',
111 comment_end_string='##}',
202 )
112 )
203
113
204 try:
114 try:
205 template = jinja_env.get_template('mercurial.iss')
115 template = jinja_env.get_template('mercurial.iss')
206 except jinja2.TemplateSyntaxError as e:
116 except jinja2.TemplateSyntaxError as e:
207 raise Exception(
117 raise Exception(
208 'template syntax error at %s:%d: %s'
118 'template syntax error at %s:%d: %s'
209 % (
119 % (
210 e.name,
120 e.name,
211 e.lineno,
121 e.lineno,
212 e.message,
122 e.message,
213 )
123 )
214 )
124 )
215
125
216 content = template.render(package_files=package_files)
126 content = template.render(package_files=package_files)
217
127
218 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
128 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
219 fh.write(content)
129 fh.write(content)
220
130
221 # Copy additional files used by Inno.
131 # Copy additional files used by Inno.
222 for p in ('mercurial.ico', 'postinstall.txt'):
132 for p in ('mercurial.ico', 'postinstall.txt'):
223 shutil.copyfile(
133 shutil.copyfile(
224 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
134 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
225 )
135 )
226
136
227 args = [str(iscc_exe)]
137 args = [str(iscc_exe)]
228
138
229 if arch:
139 if arch:
230 args.append('/dARCH=%s' % arch)
140 args.append('/dARCH=%s' % arch)
231 args.append('/dSUFFIX=-%s%s' % (arch, suffix))
141 args.append('/dSUFFIX=-%s%s' % (arch, suffix))
232 else:
142 else:
233 args.append('/dSUFFIX=-x86%s' % suffix)
143 args.append('/dSUFFIX=-x86%s' % suffix)
234
144
235 if not version:
145 if not version:
236 version = read_version_py(source_dir)
146 version = read_version_py(source_dir)
237
147
238 args.append('/dVERSION=%s' % version)
148 args.append('/dVERSION=%s' % version)
239 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
149 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
240
150
241 args.append('/Odist')
151 args.append('/Odist')
242 args.append(str(inno_build_dir / 'mercurial.iss'))
152 args.append(str(inno_build_dir / 'mercurial.iss'))
243
153
244 subprocess.run(args, cwd=str(source_dir), check=True)
154 subprocess.run(args, cwd=str(source_dir), check=True)
@@ -1,338 +1,190 b''
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
11 import getpass
12 import glob
10 import glob
13 import os
11 import os
14 import pathlib
12 import pathlib
15 import re
13 import re
16 import shutil
14 import shutil
17 import subprocess
15 import subprocess
18 import tarfile
19 import zipfile
16 import zipfile
20
17
21
18
22 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
23 with tarfile.open(source, 'r') as tf:
24 tf.extractall(dest)
25
26
27 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
19 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
28 with zipfile.ZipFile(source, 'r') as zf:
20 with zipfile.ZipFile(source, 'r') as zf:
29 zf.extractall(dest)
21 zf.extractall(dest)
30
22
31
23
32 def find_vc_runtime_dll(x64=False):
24 def find_vc_runtime_dll(x64=False):
33 """Finds Visual C++ Runtime DLL to include in distribution."""
25 """Finds Visual C++ Runtime DLL to include in distribution."""
34 # We invoke vswhere to find the latest Visual Studio install.
26 # We invoke vswhere to find the latest Visual Studio install.
35 vswhere = (
27 vswhere = (
36 pathlib.Path(os.environ["ProgramFiles(x86)"])
28 pathlib.Path(os.environ["ProgramFiles(x86)"])
37 / "Microsoft Visual Studio"
29 / "Microsoft Visual Studio"
38 / "Installer"
30 / "Installer"
39 / "vswhere.exe"
31 / "vswhere.exe"
40 )
32 )
41
33
42 if not vswhere.exists():
34 if not vswhere.exists():
43 raise Exception(
35 raise Exception(
44 "could not find vswhere.exe: %s does not exist" % vswhere
36 "could not find vswhere.exe: %s does not exist" % vswhere
45 )
37 )
46
38
47 args = [
39 args = [
48 str(vswhere),
40 str(vswhere),
49 # -products * is necessary to return results from Build Tools
41 # -products * is necessary to return results from Build Tools
50 # (as opposed to full IDE installs).
42 # (as opposed to full IDE installs).
51 "-products",
43 "-products",
52 "*",
44 "*",
53 "-requires",
45 "-requires",
54 "Microsoft.VisualCpp.Redist.14.Latest",
46 "Microsoft.VisualCpp.Redist.14.Latest",
55 "-latest",
47 "-latest",
56 "-property",
48 "-property",
57 "installationPath",
49 "installationPath",
58 ]
50 ]
59
51
60 vs_install_path = pathlib.Path(
52 vs_install_path = pathlib.Path(
61 os.fsdecode(subprocess.check_output(args).strip())
53 os.fsdecode(subprocess.check_output(args).strip())
62 )
54 )
63
55
64 # This just gets us a path like
56 # This just gets us a path like
65 # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
57 # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
66 # Actually vcruntime140.dll is under a path like:
58 # Actually vcruntime140.dll is under a path like:
67 # VC\Redist\MSVC\<version>\<arch>\Microsoft.VC14<X>.CRT\vcruntime140.dll.
59 # VC\Redist\MSVC\<version>\<arch>\Microsoft.VC14<X>.CRT\vcruntime140.dll.
68
60
69 arch = "x64" if x64 else "x86"
61 arch = "x64" if x64 else "x86"
70
62
71 search_glob = (
63 search_glob = (
72 r"%s\VC\Redist\MSVC\*\%s\Microsoft.VC14*.CRT\vcruntime140.dll"
64 r"%s\VC\Redist\MSVC\*\%s\Microsoft.VC14*.CRT\vcruntime140.dll"
73 % (vs_install_path, arch)
65 % (vs_install_path, arch)
74 )
66 )
75
67
76 candidates = glob.glob(search_glob, recursive=True)
68 candidates = glob.glob(search_glob, recursive=True)
77
69
78 for candidate in reversed(candidates):
70 for candidate in reversed(candidates):
79 return pathlib.Path(candidate)
71 return pathlib.Path(candidate)
80
72
81 raise Exception("could not find vcruntime140.dll")
73 raise Exception("could not find vcruntime140.dll")
82
74
83
75
84 def find_legacy_vc_runtime_files(x64=False):
85 """Finds Visual C++ Runtime DLLs to include in distribution."""
86 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
87
88 prefix = 'amd64' if x64 else 'x86'
89
90 candidates = sorted(
91 p
92 for p in os.listdir(winsxs)
93 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
94 )
95
96 for p in candidates:
97 print('found candidate VC runtime: %s' % p)
98
99 # Take the newest version.
100 version = candidates[-1]
101
102 d = winsxs / version
103
104 return [
105 d / 'msvcm90.dll',
106 d / 'msvcp90.dll',
107 d / 'msvcr90.dll',
108 winsxs / 'Manifests' / ('%s.manifest' % version),
109 ]
110
111
112 def windows_10_sdk_info():
113 """Resolves information about the Windows 10 SDK."""
114
115 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
116
117 if not base.is_dir():
118 raise Exception('unable to find Windows 10 SDK at %s' % base)
119
120 # Find the latest version.
121 bin_base = base / 'bin'
122
123 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
124 version = sorted(versions, reverse=True)[0]
125
126 bin_version = bin_base / version
127
128 return {
129 'root': base,
130 'version': version,
131 'bin_root': bin_version,
132 'bin_x86': bin_version / 'x86',
133 'bin_x64': bin_version / 'x64',
134 }
135
136
137 def normalize_windows_version(version):
76 def normalize_windows_version(version):
138 """Normalize Mercurial version string so WiX/Inno accepts it.
77 """Normalize Mercurial version string so WiX/Inno accepts it.
139
78
140 Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's
79 Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's
141 requirements.
80 requirements.
142
81
143 We normalize RC version or the commit count to a 4th version component.
82 We normalize RC version or the commit count to a 4th version component.
144 We store this in the 4th component because ``A.B.C`` releases do occur
83 We store this in the 4th component because ``A.B.C`` releases do occur
145 and we want an e.g. ``5.3rc0`` version to be semantically less than a
84 and we want an e.g. ``5.3rc0`` version to be semantically less than a
146 ``5.3.1rc2`` version. This requires always reserving the 3rd version
85 ``5.3.1rc2`` version. This requires always reserving the 3rd version
147 component for the point release and the ``X.YrcN`` release is always
86 component for the point release and the ``X.YrcN`` release is always
148 point release 0.
87 point release 0.
149
88
150 In the case of an RC and presence of ``+`` suffix data, we can't use both
89 In the case of an RC and presence of ``+`` suffix data, we can't use both
151 because the version format is limited to 4 components. We choose to use
90 because the version format is limited to 4 components. We choose to use
152 RC and throw away the commit count in the suffix. This means we could
91 RC and throw away the commit count in the suffix. This means we could
153 produce multiple installers with the same normalized version string.
92 produce multiple installers with the same normalized version string.
154
93
155 >>> normalize_windows_version("5.3")
94 >>> normalize_windows_version("5.3")
156 '5.3.0'
95 '5.3.0'
157
96
158 >>> normalize_windows_version("5.3rc0")
97 >>> normalize_windows_version("5.3rc0")
159 '5.3.0.0'
98 '5.3.0.0'
160
99
161 >>> normalize_windows_version("5.3rc1")
100 >>> normalize_windows_version("5.3rc1")
162 '5.3.0.1'
101 '5.3.0.1'
163
102
164 >>> normalize_windows_version("5.3rc1+hg2.abcdef")
103 >>> normalize_windows_version("5.3rc1+hg2.abcdef")
165 '5.3.0.1'
104 '5.3.0.1'
166
105
167 >>> normalize_windows_version("5.3+hg2.abcdef")
106 >>> normalize_windows_version("5.3+hg2.abcdef")
168 '5.3.0.2'
107 '5.3.0.2'
169 """
108 """
170 if '+' in version:
109 if '+' in version:
171 version, extra = version.split('+', 1)
110 version, extra = version.split('+', 1)
172 else:
111 else:
173 extra = None
112 extra = None
174
113
175 # 4.9rc0
114 # 4.9rc0
176 if version[:-1].endswith('rc'):
115 if version[:-1].endswith('rc'):
177 rc = int(version[-1:])
116 rc = int(version[-1:])
178 version = version[:-3]
117 version = version[:-3]
179 else:
118 else:
180 rc = None
119 rc = None
181
120
182 # Ensure we have at least X.Y version components.
121 # Ensure we have at least X.Y version components.
183 versions = [int(v) for v in version.split('.')]
122 versions = [int(v) for v in version.split('.')]
184 while len(versions) < 3:
123 while len(versions) < 3:
185 versions.append(0)
124 versions.append(0)
186
125
187 if len(versions) < 4:
126 if len(versions) < 4:
188 if rc is not None:
127 if rc is not None:
189 versions.append(rc)
128 versions.append(rc)
190 elif extra:
129 elif extra:
191 # hg<commit count>.<hash>+<date>
130 # hg<commit count>.<hash>+<date>
192 versions.append(int(extra.split('.')[0][2:]))
131 versions.append(int(extra.split('.')[0][2:]))
193
132
194 return '.'.join('%d' % x for x in versions[0:4])
133 return '.'.join('%d' % x for x in versions[0:4])
195
134
196
135
197 def find_signtool():
198 """Find signtool.exe from the Windows SDK."""
199 sdk = windows_10_sdk_info()
200
201 for key in ('bin_x64', 'bin_x86'):
202 p = sdk[key] / 'signtool.exe'
203
204 if p.exists():
205 return p
206
207 raise Exception('could not find signtool.exe in Windows 10 SDK')
208
209
210 def sign_with_signtool(
211 file_path,
212 description,
213 subject_name=None,
214 cert_path=None,
215 cert_password=None,
216 timestamp_url=None,
217 ):
218 """Digitally sign a file with signtool.exe.
219
220 ``file_path`` is file to sign.
221 ``description`` is text that goes in the signature.
222
223 The signing certificate can be specified by ``cert_path`` or
224 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
225 to signtool.exe, respectively.
226
227 The certificate password can be specified via ``cert_password``. If
228 not provided, you will be prompted for the password.
229
230 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
231 argument to signtool.exe).
232 """
233 if cert_path and subject_name:
234 raise ValueError('cannot specify both cert_path and subject_name')
235
236 while cert_path and not cert_password:
237 cert_password = getpass.getpass('password for %s: ' % cert_path)
238
239 args = [
240 str(find_signtool()),
241 'sign',
242 '/v',
243 '/fd',
244 'sha256',
245 '/d',
246 description,
247 ]
248
249 if cert_path:
250 args.extend(['/f', str(cert_path), '/p', cert_password])
251 elif subject_name:
252 args.extend(['/n', subject_name])
253
254 if timestamp_url:
255 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
256
257 args.append(str(file_path))
258
259 print('signing %s' % file_path)
260 subprocess.run(args, check=True)
261
262
263 PRINT_PYTHON_INFO = '''
264 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
265 '''.strip()
266
267
268 def python_exe_info(python_exe: pathlib.Path):
269 """Obtain information about a Python executable."""
270
271 res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
272
273 arch, version = res.decode('utf-8').split(':')
274
275 version = distutils.version.LooseVersion(version)
276
277 return {
278 'arch': arch,
279 'version': version,
280 'py3': version >= distutils.version.LooseVersion('3'),
281 }
282
283
284 def process_install_rules(
136 def process_install_rules(
285 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
137 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
286 ):
138 ):
287 for source, dest in rules:
139 for source, dest in rules:
288 if '*' in source:
140 if '*' in source:
289 if not dest.endswith('/'):
141 if not dest.endswith('/'):
290 raise ValueError('destination must end in / when globbing')
142 raise ValueError('destination must end in / when globbing')
291
143
292 # We strip off the source path component before the first glob
144 # We strip off the source path component before the first glob
293 # character to construct the relative install path.
145 # character to construct the relative install path.
294 prefix_end_index = source[: source.index('*')].rindex('/')
146 prefix_end_index = source[: source.index('*')].rindex('/')
295 relative_prefix = source_dir / source[0:prefix_end_index]
147 relative_prefix = source_dir / source[0:prefix_end_index]
296
148
297 for res in glob.glob(str(source_dir / source), recursive=True):
149 for res in glob.glob(str(source_dir / source), recursive=True):
298 source_path = pathlib.Path(res)
150 source_path = pathlib.Path(res)
299
151
300 if source_path.is_dir():
152 if source_path.is_dir():
301 continue
153 continue
302
154
303 rel_path = source_path.relative_to(relative_prefix)
155 rel_path = source_path.relative_to(relative_prefix)
304
156
305 dest_path = dest_dir / dest[:-1] / rel_path
157 dest_path = dest_dir / dest[:-1] / rel_path
306
158
307 dest_path.parent.mkdir(parents=True, exist_ok=True)
159 dest_path.parent.mkdir(parents=True, exist_ok=True)
308 print('copying %s to %s' % (source_path, dest_path))
160 print('copying %s to %s' % (source_path, dest_path))
309 shutil.copy(source_path, dest_path)
161 shutil.copy(source_path, dest_path)
310
162
311 # Simple file case.
163 # Simple file case.
312 else:
164 else:
313 source_path = pathlib.Path(source)
165 source_path = pathlib.Path(source)
314
166
315 if dest.endswith('/'):
167 if dest.endswith('/'):
316 dest_path = pathlib.Path(dest) / source_path.name
168 dest_path = pathlib.Path(dest) / source_path.name
317 else:
169 else:
318 dest_path = pathlib.Path(dest)
170 dest_path = pathlib.Path(dest)
319
171
320 full_source_path = source_dir / source_path
172 full_source_path = source_dir / source_path
321 full_dest_path = dest_dir / dest_path
173 full_dest_path = dest_dir / dest_path
322
174
323 full_dest_path.parent.mkdir(parents=True, exist_ok=True)
175 full_dest_path.parent.mkdir(parents=True, exist_ok=True)
324 shutil.copy(full_source_path, full_dest_path)
176 shutil.copy(full_source_path, full_dest_path)
325 print('copying %s to %s' % (full_source_path, full_dest_path))
177 print('copying %s to %s' % (full_source_path, full_dest_path))
326
178
327
179
328 def read_version_py(source_dir):
180 def read_version_py(source_dir):
329 """Read the mercurial/__version__.py file to resolve the version string."""
181 """Read the mercurial/__version__.py file to resolve the version string."""
330 p = source_dir / 'mercurial' / '__version__.py'
182 p = source_dir / 'mercurial' / '__version__.py'
331
183
332 with p.open('r', encoding='utf-8') as fh:
184 with p.open('r', encoding='utf-8') as fh:
333 m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
185 m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
334
186
335 if not m:
187 if not m:
336 raise Exception('could not parse %s' % p)
188 raise Exception('could not parse %s' % p)
337
189
338 return m.group(1)
190 return m.group(1)
@@ -1,586 +1,96 b''
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 collections
11 import json
10 import json
12 import os
11 import os
13 import pathlib
12 import pathlib
14 import re
15 import shutil
13 import shutil
16 import subprocess
17 import typing
14 import typing
18 import uuid
19 import xml.dom.minidom
20
15
21 from .downloads import download_entry
22 from .py2exe import (
23 build_py2exe,
24 stage_install,
25 )
26 from .pyoxidizer import (
16 from .pyoxidizer import (
27 build_docs_html,
17 build_docs_html,
28 create_pyoxidizer_install_layout,
29 run_pyoxidizer,
18 run_pyoxidizer,
30 )
19 )
31 from .util import (
32 extract_zip_to_directory,
33 normalize_windows_version,
34 process_install_rules,
35 sign_with_signtool,
36 )
37
38
39 EXTRA_PACKAGES = {
40 'dulwich',
41 'distutils',
42 'keyring',
43 'pygments',
44 'win32ctypes',
45 }
46
47 EXTRA_INCLUDES = {
48 '_curses',
49 '_curses_panel',
50 }
51
52 EXTRA_INSTALL_RULES = [
53 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
54 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
55 ]
56
57 STAGING_REMOVE_FILES = [
58 # We use the RTF variant.
59 'copying.txt',
60 ]
61
62 SHORTCUTS = {
63 # hg.1.html'
64 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
65 'Name': 'Mercurial Command Reference',
66 },
67 # hgignore.5.html
68 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
69 'Name': 'Mercurial Ignore Files',
70 },
71 # hgrc.5.html
72 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
73 'Name': 'Mercurial Configuration Files',
74 },
75 }
76
77
78 def find_version(source_dir: pathlib.Path):
79 version_py = source_dir / 'mercurial' / '__version__.py'
80
81 with version_py.open('r', encoding='utf-8') as fh:
82 source = fh.read().strip()
83
84 m = re.search('version = b"(.*)"', source)
85 return m.group(1)
86
87
88 def ensure_vc90_merge_modules(build_dir):
89 x86 = (
90 download_entry(
91 'vc9-crt-x86-msm',
92 build_dir,
93 local_name='microsoft.vcxx.crt.x86_msm.msm',
94 )[0],
95 download_entry(
96 'vc9-crt-x86-msm-policy',
97 build_dir,
98 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
99 )[0],
100 )
101
102 x64 = (
103 download_entry(
104 'vc9-crt-x64-msm',
105 build_dir,
106 local_name='microsoft.vcxx.crt.x64_msm.msm',
107 )[0],
108 download_entry(
109 'vc9-crt-x64-msm-policy',
110 build_dir,
111 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
112 )[0],
113 )
114 return {
115 'x86': x86,
116 'x64': x64,
117 }
118
119
120 def run_candle(wix, cwd, wxs, source_dir, defines=None):
121 args = [
122 str(wix / 'candle.exe'),
123 '-nologo',
124 str(wxs),
125 '-dSourceDir=%s' % source_dir,
126 ]
127
128 if defines:
129 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
130
131 subprocess.run(args, cwd=str(cwd), check=True)
132
133
134 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
135 """Create XML string listing every file to be installed."""
136
137 # We derive GUIDs from a deterministic file path identifier.
138 # We shoehorn the name into something that looks like a URL because
139 # the UUID namespaces are supposed to work that way (even though
140 # the input data probably is never validated).
141
142 doc = xml.dom.minidom.parseString(
143 '<?xml version="1.0" encoding="utf-8"?>'
144 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
145 '</Wix>'
146 )
147
148 # Assemble the install layout by directory. This makes it easier to
149 # emit XML, since each directory has separate entities.
150 manifest = collections.defaultdict(dict)
151
152 for root, dirs, files in os.walk(staging_dir):
153 dirs.sort()
154
155 root = pathlib.Path(root)
156 rel_dir = root.relative_to(staging_dir)
157
158 for i in range(len(rel_dir.parts)):
159 parent = '/'.join(rel_dir.parts[0 : i + 1])
160 manifest.setdefault(parent, {})
161
162 for f in sorted(files):
163 full = root / f
164 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
165
166 component_groups = collections.defaultdict(list)
167
168 # Now emit a <Fragment> for each directory.
169 # Each directory is composed of a <DirectoryRef> pointing to its parent
170 # and defines child <Directory>'s and a <Component> with all the files.
171 for dir_name, entries in sorted(manifest.items()):
172 # The directory id is derived from the path. But the root directory
173 # is special.
174 if dir_name == '.':
175 parent_directory_id = 'INSTALLDIR'
176 else:
177 parent_directory_id = 'hg.dir.%s' % dir_name.replace(
178 '/', '.'
179 ).replace('-', '_')
180
181 fragment = doc.createElement('Fragment')
182 directory_ref = doc.createElement('DirectoryRef')
183 directory_ref.setAttribute('Id', parent_directory_id)
184
185 # Add <Directory> entries for immediate children directories.
186 for possible_child in sorted(manifest.keys()):
187 if (
188 dir_name == '.'
189 and '/' not in possible_child
190 and possible_child != '.'
191 ):
192 child_directory_id = ('hg.dir.%s' % possible_child).replace(
193 '-', '_'
194 )
195 name = possible_child
196 else:
197 if not possible_child.startswith('%s/' % dir_name):
198 continue
199 name = possible_child[len(dir_name) + 1 :]
200 if '/' in name:
201 continue
202
203 child_directory_id = 'hg.dir.%s' % possible_child.replace(
204 '/', '.'
205 ).replace('-', '_')
206
207 directory = doc.createElement('Directory')
208 directory.setAttribute('Id', child_directory_id)
209 directory.setAttribute('Name', name)
210 directory_ref.appendChild(directory)
211
212 # Add <Component>s for files in this directory.
213 for rel, source_path in sorted(entries.items()):
214 if dir_name == '.':
215 full_rel = rel
216 else:
217 full_rel = '%s/%s' % (dir_name, rel)
218
219 component_unique_id = (
220 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
221 % full_rel
222 )
223 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
224 component_id = 'hg.component.%s' % str(component_guid).replace(
225 '-', '_'
226 )
227
228 component = doc.createElement('Component')
229
230 component.setAttribute('Id', component_id)
231 component.setAttribute('Guid', str(component_guid).upper())
232 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
233
234 # Assign this component to a top-level group.
235 if dir_name == '.':
236 component_groups['ROOT'].append(component_id)
237 elif '/' in dir_name:
238 component_groups[dir_name[0 : dir_name.index('/')]].append(
239 component_id
240 )
241 else:
242 component_groups[dir_name].append(component_id)
243
244 unique_id = (
245 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
246 )
247 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
248
249 # IDs have length limits. So use GUID to derive them.
250 file_guid_normalized = str(file_guid).replace('-', '_')
251 file_id = 'hg.file.%s' % file_guid_normalized
252
253 file_element = doc.createElement('File')
254 file_element.setAttribute('Id', file_id)
255 file_element.setAttribute('Source', str(source_path))
256 file_element.setAttribute('KeyPath', 'yes')
257 file_element.setAttribute('ReadOnly', 'yes')
258
259 component.appendChild(file_element)
260 directory_ref.appendChild(component)
261
262 fragment.appendChild(directory_ref)
263 doc.documentElement.appendChild(fragment)
264
265 for group, component_ids in sorted(component_groups.items()):
266 fragment = doc.createElement('Fragment')
267 component_group = doc.createElement('ComponentGroup')
268 component_group.setAttribute('Id', 'hg.group.%s' % group)
269
270 for component_id in component_ids:
271 component_ref = doc.createElement('ComponentRef')
272 component_ref.setAttribute('Id', component_id)
273 component_group.appendChild(component_ref)
274
275 fragment.appendChild(component_group)
276 doc.documentElement.appendChild(fragment)
277
278 # Add <Shortcut> to files that have it defined.
279 for file_id, metadata in sorted(SHORTCUTS.items()):
280 els = doc.getElementsByTagName('File')
281 els = [el for el in els if el.getAttribute('Id') == file_id]
282
283 if not els:
284 raise Exception('could not find File[Id=%s]' % file_id)
285
286 for el in els:
287 shortcut = doc.createElement('Shortcut')
288 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
289 shortcut.setAttribute('Directory', 'ProgramMenuDir')
290 shortcut.setAttribute('Icon', 'hgIcon.ico')
291 shortcut.setAttribute('IconIndex', '0')
292 shortcut.setAttribute('Advertise', 'yes')
293 for k, v in sorted(metadata.items()):
294 shortcut.setAttribute(k, v)
295
296 el.appendChild(shortcut)
297
298 return doc.toprettyxml()
299
300
301 def build_installer_py2exe(
302 source_dir: pathlib.Path,
303 python_exe: pathlib.Path,
304 msi_name='mercurial',
305 version=None,
306 extra_packages_script=None,
307 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
308 extra_features: typing.Optional[typing.List[str]] = None,
309 signing_info: typing.Optional[typing.Dict[str, str]] = None,
310 ):
311 """Build a WiX MSI installer using py2exe.
312
313 ``source_dir`` is the path to the Mercurial source tree to use.
314 ``arch`` is the target architecture. either ``x86`` or ``x64``.
315 ``python_exe`` is the path to the Python executable to use/bundle.
316 ``version`` is the Mercurial version string. If not defined,
317 ``mercurial/__version__.py`` will be consulted.
318 ``extra_packages_script`` is a command to be run to inject extra packages
319 into the py2exe binary. It should stage packages into the virtualenv and
320 print a null byte followed by a newline-separated list of packages that
321 should be included in the exe.
322 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
323 ``extra_features`` is a list of additional named Features to include in
324 the build. These must match Feature names in one of the wxs scripts.
325 """
326 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
327
328 hg_build_dir = source_dir / 'build'
329
330 requirements_txt = (
331 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
332 )
333
334 build_py2exe(
335 source_dir,
336 hg_build_dir,
337 python_exe,
338 'wix',
339 requirements_txt,
340 extra_packages=EXTRA_PACKAGES,
341 extra_packages_script=extra_packages_script,
342 extra_includes=EXTRA_INCLUDES,
343 )
344
345 build_dir = hg_build_dir / ('wix-%s' % arch)
346 staging_dir = build_dir / 'stage'
347
348 build_dir.mkdir(exist_ok=True)
349
350 # Purge the staging directory for every build so packaging is pristine.
351 if staging_dir.exists():
352 print('purging %s' % staging_dir)
353 shutil.rmtree(staging_dir)
354
355 stage_install(source_dir, staging_dir, lower_case=True)
356
357 # We also install some extra files.
358 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
359
360 # And remove some files we don't want.
361 for f in STAGING_REMOVE_FILES:
362 p = staging_dir / f
363 if p.exists():
364 print('removing %s' % p)
365 p.unlink()
366
367 return run_wix_packaging(
368 source_dir,
369 build_dir,
370 staging_dir,
371 arch,
372 version=version,
373 python2=True,
374 msi_name=msi_name,
375 suffix="-python2",
376 extra_wxs=extra_wxs,
377 extra_features=extra_features,
378 signing_info=signing_info,
379 )
380
20
381
21
382 def build_installer_pyoxidizer(
22 def build_installer_pyoxidizer(
383 source_dir: pathlib.Path,
23 source_dir: pathlib.Path,
384 target_triple: str,
24 target_triple: str,
385 msi_name='mercurial',
25 msi_name='mercurial',
386 version=None,
26 version=None,
387 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
27 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
388 extra_features: typing.Optional[typing.List[str]] = None,
28 extra_features: typing.Optional[typing.List[str]] = None,
389 signing_info: typing.Optional[typing.Dict[str, str]] = None,
29 signing_info: typing.Optional[typing.Dict[str, str]] = None,
390 extra_pyoxidizer_vars=None,
30 extra_pyoxidizer_vars=None,
391 ):
31 ):
392 """Build a WiX MSI installer using PyOxidizer."""
32 """Build a WiX MSI installer using PyOxidizer."""
393 hg_build_dir = source_dir / "build"
33 hg_build_dir = source_dir / "build"
394 build_dir = hg_build_dir / ("wix-%s" % target_triple)
34 build_dir = hg_build_dir / ("wix-%s" % target_triple)
395
35
396 build_dir.mkdir(parents=True, exist_ok=True)
36 build_dir.mkdir(parents=True, exist_ok=True)
397
37
398 # Need to ensure docs HTML is built because this isn't done as part of
38 # Need to ensure docs HTML is built because this isn't done as part of
399 # `pip install Mercurial`.
39 # `pip install Mercurial`.
400 build_docs_html(source_dir)
40 build_docs_html(source_dir)
401
41
402 build_vars = {}
42 build_vars = {}
403
43
404 if msi_name:
44 if msi_name:
405 build_vars["MSI_NAME"] = msi_name
45 build_vars["MSI_NAME"] = msi_name
406
46
407 if version:
47 if version:
408 build_vars["VERSION"] = version
48 build_vars["VERSION"] = version
409
49
410 if extra_features:
50 if extra_features:
411 build_vars["EXTRA_MSI_FEATURES"] = ";".join(extra_features)
51 build_vars["EXTRA_MSI_FEATURES"] = ";".join(extra_features)
412
52
413 if signing_info:
53 if signing_info:
414 if signing_info["cert_path"]:
54 if signing_info["cert_path"]:
415 build_vars["SIGNING_PFX_PATH"] = signing_info["cert_path"]
55 build_vars["SIGNING_PFX_PATH"] = signing_info["cert_path"]
416 if signing_info["cert_password"]:
56 if signing_info["cert_password"]:
417 build_vars["SIGNING_PFX_PASSWORD"] = signing_info["cert_password"]
57 build_vars["SIGNING_PFX_PASSWORD"] = signing_info["cert_password"]
418 if signing_info["subject_name"]:
58 if signing_info["subject_name"]:
419 build_vars["SIGNING_SUBJECT_NAME"] = signing_info["subject_name"]
59 build_vars["SIGNING_SUBJECT_NAME"] = signing_info["subject_name"]
420 if signing_info["timestamp_url"]:
60 if signing_info["timestamp_url"]:
421 build_vars["TIME_STAMP_SERVER_URL"] = signing_info["timestamp_url"]
61 build_vars["TIME_STAMP_SERVER_URL"] = signing_info["timestamp_url"]
422
62
423 if extra_pyoxidizer_vars:
63 if extra_pyoxidizer_vars:
424 build_vars.update(json.loads(extra_pyoxidizer_vars))
64 build_vars.update(json.loads(extra_pyoxidizer_vars))
425
65
426 if extra_wxs:
66 if extra_wxs:
427 raise Exception(
67 raise Exception(
428 "support for extra .wxs files has been temporarily dropped"
68 "support for extra .wxs files has been temporarily dropped"
429 )
69 )
430
70
431 out_dir = run_pyoxidizer(
71 out_dir = run_pyoxidizer(
432 source_dir,
72 source_dir,
433 build_dir,
73 build_dir,
434 target_triple,
74 target_triple,
435 build_vars=build_vars,
75 build_vars=build_vars,
436 target="msi",
76 target="msi",
437 )
77 )
438
78
439 msi_dir = out_dir / "msi"
79 msi_dir = out_dir / "msi"
440 msi_files = [f for f in os.listdir(msi_dir) if f.endswith(".msi")]
80 msi_files = [f for f in os.listdir(msi_dir) if f.endswith(".msi")]
441
81
442 if len(msi_files) != 1:
82 if len(msi_files) != 1:
443 raise Exception("expected exactly 1 .msi file; got %d" % len(msi_files))
83 raise Exception("expected exactly 1 .msi file; got %d" % len(msi_files))
444
84
445 msi_filename = msi_files[0]
85 msi_filename = msi_files[0]
446
86
447 msi_path = msi_dir / msi_filename
87 msi_path = msi_dir / msi_filename
448 dist_path = source_dir / "dist" / msi_filename
88 dist_path = source_dir / "dist" / msi_filename
449
89
450 dist_path.parent.mkdir(parents=True, exist_ok=True)
90 dist_path.parent.mkdir(parents=True, exist_ok=True)
451
91
452 shutil.copyfile(msi_path, dist_path)
92 shutil.copyfile(msi_path, dist_path)
453
93
454 return {
94 return {
455 "msi_path": dist_path,
95 "msi_path": dist_path,
456 }
96 }
457
458
459 def run_wix_packaging(
460 source_dir: pathlib.Path,
461 build_dir: pathlib.Path,
462 staging_dir: pathlib.Path,
463 arch: str,
464 version: str,
465 python2: bool,
466 msi_name: typing.Optional[str] = "mercurial",
467 suffix: str = "",
468 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
469 extra_features: typing.Optional[typing.List[str]] = None,
470 signing_info: typing.Optional[typing.Dict[str, str]] = None,
471 ):
472 """Invokes WiX to package up a built Mercurial.
473
474 ``signing_info`` is a dict defining properties to facilitate signing the
475 installer. Recognized keys include ``name``, ``subject_name``,
476 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
477 we will sign both the hg.exe and the .msi using the signing credentials
478 specified.
479 """
480
481 orig_version = version or find_version(source_dir)
482 version = normalize_windows_version(orig_version)
483 print('using version string: %s' % version)
484 if version != orig_version:
485 print('(normalized from: %s)' % orig_version)
486
487 if signing_info:
488 sign_with_signtool(
489 staging_dir / "hg.exe",
490 "%s %s" % (signing_info["name"], version),
491 subject_name=signing_info["subject_name"],
492 cert_path=signing_info["cert_path"],
493 cert_password=signing_info["cert_password"],
494 timestamp_url=signing_info["timestamp_url"],
495 )
496
497 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
498
499 wix_pkg, wix_entry = download_entry('wix', build_dir)
500 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
501
502 if not wix_path.exists():
503 extract_zip_to_directory(wix_pkg, wix_path)
504
505 if python2:
506 ensure_vc90_merge_modules(build_dir)
507
508 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
509
510 defines = {'Platform': arch}
511
512 # Derive a .wxs file with the staged files.
513 manifest_wxs = build_dir / 'stage.wxs'
514 with manifest_wxs.open('w', encoding='utf-8') as fh:
515 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
516
517 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
518
519 for source, rel_path in sorted((extra_wxs or {}).items()):
520 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
521
522 source = wix_dir / 'mercurial.wxs'
523 defines['Version'] = version
524 defines['Comments'] = 'Installs Mercurial version %s' % version
525
526 if python2:
527 defines["PythonVersion"] = "2"
528 defines['VCRedistSrcDir'] = str(build_dir)
529 else:
530 defines["PythonVersion"] = "3"
531
532 if (staging_dir / "lib").exists():
533 defines["MercurialHasLib"] = "1"
534
535 if extra_features:
536 assert all(';' not in f for f in extra_features)
537 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
538
539 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
540
541 msi_path = (
542 source_dir
543 / 'dist'
544 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
545 )
546
547 args = [
548 str(wix_path / 'light.exe'),
549 '-nologo',
550 '-ext',
551 'WixUIExtension',
552 '-sw1076',
553 '-spdb',
554 '-o',
555 str(msi_path),
556 ]
557
558 for source, rel_path in sorted((extra_wxs or {}).items()):
559 assert source.endswith('.wxs')
560 source = os.path.basename(source)
561 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
562
563 args.extend(
564 [
565 str(build_dir / 'stage.wixobj'),
566 str(build_dir / 'mercurial.wixobj'),
567 ]
568 )
569
570 subprocess.run(args, cwd=str(source_dir), check=True)
571
572 print('%s created' % msi_path)
573
574 if signing_info:
575 sign_with_signtool(
576 msi_path,
577 "%s %s" % (signing_info["name"], version),
578 subject_name=signing_info["subject_name"],
579 cert_path=signing_info["cert_path"],
580 cert_password=signing_info["cert_password"],
581 timestamp_url=signing_info["timestamp_url"],
582 )
583
584 return {
585 'msi_path': msi_path,
586 }
@@ -1,96 +1,95 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4 $ check_code="$TESTDIR"/../contrib/check-code.py
4 $ check_code="$TESTDIR"/../contrib/check-code.py
5 $ cd "$TESTDIR"/..
5 $ cd "$TESTDIR"/..
6
6
7 New errors are not allowed. Warnings are strongly discouraged.
7 New errors are not allowed. Warnings are strongly discouraged.
8 (The writing "no-che?k-code" is for not skipping this file when checking.)
8 (The writing "no-che?k-code" is for not skipping this file when checking.)
9
9
10 $ testrepohg locate \
10 $ testrepohg locate \
11 > -X contrib/python-zstandard \
11 > -X contrib/python-zstandard \
12 > -X hgext/fsmonitor/pywatchman \
12 > -X hgext/fsmonitor/pywatchman \
13 > -X mercurial/thirdparty \
13 > -X mercurial/thirdparty \
14 > -X mercurial/pythoncapi_compat.h \
14 > -X mercurial/pythoncapi_compat.h \
15 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
15 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
16 Skipping contrib/automation/hgautomation/__init__.py it has no-che?k-code (glob)
16 Skipping contrib/automation/hgautomation/__init__.py it has no-che?k-code (glob)
17 Skipping contrib/automation/hgautomation/aws.py it has no-che?k-code (glob)
17 Skipping contrib/automation/hgautomation/aws.py it has no-che?k-code (glob)
18 Skipping contrib/automation/hgautomation/cli.py it has no-che?k-code (glob)
18 Skipping contrib/automation/hgautomation/cli.py it has no-che?k-code (glob)
19 Skipping contrib/automation/hgautomation/linux.py it has no-che?k-code (glob)
19 Skipping contrib/automation/hgautomation/linux.py it has no-che?k-code (glob)
20 Skipping contrib/automation/hgautomation/pypi.py it has no-che?k-code (glob)
20 Skipping contrib/automation/hgautomation/pypi.py it has no-che?k-code (glob)
21 Skipping contrib/automation/hgautomation/ssh.py it has no-che?k-code (glob)
21 Skipping contrib/automation/hgautomation/ssh.py it has no-che?k-code (glob)
22 Skipping contrib/automation/hgautomation/try_server.py it has no-che?k-code (glob)
22 Skipping contrib/automation/hgautomation/try_server.py it has no-che?k-code (glob)
23 Skipping contrib/automation/hgautomation/windows.py it has no-che?k-code (glob)
23 Skipping contrib/automation/hgautomation/windows.py it has no-che?k-code (glob)
24 Skipping contrib/automation/hgautomation/winrm.py it has no-che?k-code (glob)
24 Skipping contrib/automation/hgautomation/winrm.py it has no-che?k-code (glob)
25 Skipping contrib/fuzz/FuzzedDataProvider.h it has no-che?k-code (glob)
25 Skipping contrib/fuzz/FuzzedDataProvider.h it has no-che?k-code (glob)
26 Skipping contrib/fuzz/standalone_fuzz_target_runner.cc it has no-che?k-code (glob)
26 Skipping contrib/fuzz/standalone_fuzz_target_runner.cc it has no-che?k-code (glob)
27 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
27 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
31 Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
32 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
31 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
33 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
32 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
34 Skipping i18n/polib.py it has no-che?k-code (glob)
33 Skipping i18n/polib.py it has no-che?k-code (glob)
35 Skipping mercurial/statprof.py it has no-che?k-code (glob)
34 Skipping mercurial/statprof.py it has no-che?k-code (glob)
36 Skipping tests/testlib/badserverext.py it has no-che?k-code (glob)
35 Skipping tests/testlib/badserverext.py it has no-che?k-code (glob)
37
36
38 @commands in debugcommands.py should be in alphabetical order.
37 @commands in debugcommands.py should be in alphabetical order.
39
38
40 >>> import re
39 >>> import re
41 >>> commands = []
40 >>> commands = []
42 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
41 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
43 ... for line in fh:
42 ... for line in fh:
44 ... m = re.match(br"^@command\('([a-z]+)", line)
43 ... m = re.match(br"^@command\('([a-z]+)", line)
45 ... if m:
44 ... if m:
46 ... commands.append(m.group(1))
45 ... commands.append(m.group(1))
47 >>> scommands = list(sorted(commands))
46 >>> scommands = list(sorted(commands))
48 >>> for i, command in enumerate(scommands):
47 >>> for i, command in enumerate(scommands):
49 ... if command != commands[i]:
48 ... if command != commands[i]:
50 ... print('commands in debugcommands.py not sorted; first differing '
49 ... print('commands in debugcommands.py not sorted; first differing '
51 ... 'command is %s; expected %s' % (commands[i], command))
50 ... 'command is %s; expected %s' % (commands[i], command))
52 ... break
51 ... break
53
52
54 Prevent adding new files in the root directory accidentally.
53 Prevent adding new files in the root directory accidentally.
55
54
56 $ testrepohg files 'glob:*'
55 $ testrepohg files 'glob:*'
57 .arcconfig
56 .arcconfig
58 .clang-format
57 .clang-format
59 .editorconfig
58 .editorconfig
60 .hgignore
59 .hgignore
61 .hgsigs
60 .hgsigs
62 .hgtags
61 .hgtags
63 .jshintrc
62 .jshintrc
64 CONTRIBUTING
63 CONTRIBUTING
65 CONTRIBUTORS
64 CONTRIBUTORS
66 COPYING
65 COPYING
67 Makefile
66 Makefile
68 README.rst
67 README.rst
69 hg
68 hg
70 hgeditor
69 hgeditor
71 hgweb.cgi
70 hgweb.cgi
72 pyproject.toml
71 pyproject.toml
73 rustfmt.toml
72 rustfmt.toml
74 setup.py
73 setup.py
75
74
76 Prevent adding modules which could be shadowed by ancient .so/.dylib.
75 Prevent adding modules which could be shadowed by ancient .so/.dylib.
77
76
78 $ testrepohg files \
77 $ testrepohg files \
79 > mercurial/base85.py \
78 > mercurial/base85.py \
80 > mercurial/bdiff.py \
79 > mercurial/bdiff.py \
81 > mercurial/diffhelpers.py \
80 > mercurial/diffhelpers.py \
82 > mercurial/mpatch.py \
81 > mercurial/mpatch.py \
83 > mercurial/osutil.py \
82 > mercurial/osutil.py \
84 > mercurial/parsers.py \
83 > mercurial/parsers.py \
85 > mercurial/zstd.py
84 > mercurial/zstd.py
86 [1]
85 [1]
87
86
88 Keep python3 tests sorted:
87 Keep python3 tests sorted:
89 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
88 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
90 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
89 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
91
90
92 Keep Windows line endings in check
91 Keep Windows line endings in check
93
92
94 $ testrepohg files 'set:eol(dos)'
93 $ testrepohg files 'set:eol(dos)'
95 contrib/win32/hg.bat
94 contrib/win32/hg.bat
96 contrib/win32/mercurial.ini
95 contrib/win32/mercurial.ini
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now