##// END OF EJS Templates
packaging: support building Inno installer with PyOxidizer...
Gregory Szorc -
r45270:94f4f2ec stable
parent child Browse files
Show More
@@ -0,0 +1,145
1 # pyoxidizer.py - Packaging support for PyOxidizer
2 #
3 # Copyright 2020 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import os
11 import pathlib
12 import shutil
13 import subprocess
14 import sys
15
16 from .downloads import download_entry
17 from .util import (
18 extract_zip_to_directory,
19 process_install_rules,
20 find_vc_runtime_dll,
21 )
22
23
24 STAGING_RULES_WINDOWS = [
25 ('contrib/bash_completion', 'contrib/'),
26 ('contrib/hgk', 'contrib/hgk.tcl'),
27 ('contrib/hgweb.fcgi', 'contrib/'),
28 ('contrib/hgweb.wsgi', 'contrib/'),
29 ('contrib/logo-droplets.svg', 'contrib/'),
30 ('contrib/mercurial.el', 'contrib/'),
31 ('contrib/mq.el', 'contrib/'),
32 ('contrib/tcsh_completion', 'contrib/'),
33 ('contrib/tcsh_completion_build.sh', 'contrib/'),
34 ('contrib/vim/*', 'contrib/vim/'),
35 ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
36 ('contrib/win32/ReadMe.html', 'ReadMe.html'),
37 ('contrib/xml.rnc', 'contrib/'),
38 ('contrib/zsh_completion', 'contrib/'),
39 ('doc/*.html', 'doc/'),
40 ('doc/style.css', 'doc/'),
41 ('COPYING', 'Copying.txt'),
42 ]
43
44 STAGING_RULES_APP = [
45 ('mercurial/helptext/**/*.txt', 'helptext/'),
46 ('mercurial/defaultrc/*.rc', 'defaultrc/'),
47 ('mercurial/locale/**/*', 'locale/'),
48 ('mercurial/templates/**/*', 'templates/'),
49 ]
50
51 STAGING_EXCLUDES_WINDOWS = [
52 "doc/hg-ssh.8.html",
53 ]
54
55
56 def run_pyoxidizer(
57 source_dir: pathlib.Path,
58 build_dir: pathlib.Path,
59 out_dir: pathlib.Path,
60 target_triple: str,
61 ):
62 """Build Mercurial with PyOxidizer and copy additional files into place.
63
64 After successful completion, ``out_dir`` contains files constituting a
65 Mercurial install.
66 """
67 # We need to make gettext binaries available for compiling i18n files.
68 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
69 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
70
71 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
72
73 if not gettext_root.exists():
74 extract_zip_to_directory(gettext_pkg, gettext_root)
75 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
76
77 env = dict(os.environ)
78 env["PATH"] = "%s%s%s" % (
79 env["PATH"],
80 os.pathsep,
81 str(gettext_root / "bin"),
82 )
83
84 args = [
85 "pyoxidizer",
86 "build",
87 "--path",
88 str(source_dir / "rust" / "hgcli"),
89 "--release",
90 "--target-triple",
91 target_triple,
92 ]
93
94 subprocess.run(args, env=env, check=True)
95
96 if "windows" in target_triple:
97 target = "app_windows"
98 else:
99 target = "app_posix"
100
101 build_dir = (
102 source_dir / "build" / "pyoxidizer" / target_triple / "release" / target
103 )
104
105 if out_dir.exists():
106 print("purging %s" % out_dir)
107 shutil.rmtree(out_dir)
108
109 # Now assemble all the files from PyOxidizer into the staging directory.
110 shutil.copytree(build_dir, out_dir)
111
112 # Move some of those files around.
113 process_install_rules(STAGING_RULES_APP, build_dir, out_dir)
114 # Nuke the mercurial/* directory, as we copied resources
115 # to an appropriate location just above.
116 shutil.rmtree(out_dir / "mercurial")
117
118 # We also need to run setup.py build_doc to produce html files,
119 # as they aren't built as part of ``pip install``.
120 # This will fail if docutils isn't installed.
121 subprocess.run(
122 [sys.executable, str(source_dir / "setup.py"), "build_doc", "--html"],
123 cwd=str(source_dir),
124 check=True,
125 )
126
127 if "windows" in target_triple:
128 process_install_rules(STAGING_RULES_WINDOWS, source_dir, out_dir)
129
130 # Write out a default editor.rc file to configure notepad as the
131 # default editor.
132 with (out_dir / "defaultrc" / "editor.rc").open(
133 "w", encoding="utf-8"
134 ) as fh:
135 fh.write("[ui]\neditor = notepad\n")
136
137 for f in STAGING_EXCLUDES_WINDOWS:
138 p = out_dir / f
139 if p.exists():
140 print("removing %s" % p)
141 p.unlink()
142
143 # Add vcruntimeXXX.dll next to executable.
144 vc_runtime_dll = find_vc_runtime_dll(x64="x86_64" in target_triple)
145 shutil.copy(vc_runtime_dll, out_dir / vc_runtime_dll.name)
@@ -1,153 +1,166
1 1 # cli.py - Command line interface for automation
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import argparse
11 11 import os
12 12 import pathlib
13 13
14 14 from . import (
15 15 inno,
16 16 wix,
17 17 )
18 18
19 19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
20 20 SOURCE_DIR = HERE.parent.parent.parent
21 21
22 22
23 def build_inno(python=None, iscc=None, version=None):
24 if not os.path.isabs(python):
23 def build_inno(pyoxidizer_target=None, python=None, 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):
25 28 raise Exception("--python arg must be an absolute path")
26 29
27 30 if iscc:
28 31 iscc = pathlib.Path(iscc)
29 32 else:
30 33 iscc = (
31 34 pathlib.Path(os.environ["ProgramFiles(x86)"])
32 35 / "Inno Setup 5"
33 36 / "ISCC.exe"
34 37 )
35 38
36 39 build_dir = SOURCE_DIR / "build"
37 40
38 inno.build(
39 SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
40 )
41 if pyoxidizer_target:
42 inno.build_with_pyoxidizer(
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
44 )
45 else:
46 inno.build_with_py2exe(
47 SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
48 )
41 49
42 50
43 51 def build_wix(
44 52 name=None,
45 53 python=None,
46 54 version=None,
47 55 sign_sn=None,
48 56 sign_cert=None,
49 57 sign_password=None,
50 58 sign_timestamp_url=None,
51 59 extra_packages_script=None,
52 60 extra_wxs=None,
53 61 extra_features=None,
54 62 ):
55 63 fn = wix.build_installer
56 64 kwargs = {
57 65 "source_dir": SOURCE_DIR,
58 66 "python_exe": pathlib.Path(python),
59 67 "version": version,
60 68 }
61 69
62 70 if not os.path.isabs(python):
63 71 raise Exception("--python arg must be an absolute path")
64 72
65 73 if extra_packages_script:
66 74 kwargs["extra_packages_script"] = extra_packages_script
67 75 if extra_wxs:
68 76 kwargs["extra_wxs"] = dict(
69 77 thing.split("=") for thing in extra_wxs.split(",")
70 78 )
71 79 if extra_features:
72 80 kwargs["extra_features"] = extra_features.split(",")
73 81
74 82 if sign_sn or sign_cert:
75 83 fn = wix.build_signed_installer
76 84 kwargs["name"] = name
77 85 kwargs["subject_name"] = sign_sn
78 86 kwargs["cert_path"] = sign_cert
79 87 kwargs["cert_password"] = sign_password
80 88 kwargs["timestamp_url"] = sign_timestamp_url
81 89
82 90 fn(**kwargs)
83 91
84 92
85 93 def get_parser():
86 94 parser = argparse.ArgumentParser()
87 95
88 96 subparsers = parser.add_subparsers()
89 97
90 98 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
91 sp.add_argument("--python", required=True, help="path to python.exe to use")
99 sp.add_argument(
100 "--pyoxidizer-target",
101 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
102 help="Build with PyOxidizer targeting this host triple",
103 )
104 sp.add_argument("--python", help="path to python.exe to use")
92 105 sp.add_argument("--iscc", help="path to iscc.exe to use")
93 106 sp.add_argument(
94 107 "--version",
95 108 help="Mercurial version string to use "
96 109 "(detected from __version__.py if not defined",
97 110 )
98 111 sp.set_defaults(func=build_inno)
99 112
100 113 sp = subparsers.add_parser(
101 114 "wix", help="Build Windows installer with WiX Toolset"
102 115 )
103 116 sp.add_argument("--name", help="Application name", default="Mercurial")
104 117 sp.add_argument(
105 118 "--python", help="Path to Python executable to use", required=True
106 119 )
107 120 sp.add_argument(
108 121 "--sign-sn",
109 122 help="Subject name (or fragment thereof) of certificate "
110 123 "to use for signing",
111 124 )
112 125 sp.add_argument(
113 126 "--sign-cert", help="Path to certificate to use for signing"
114 127 )
115 128 sp.add_argument("--sign-password", help="Password for signing certificate")
116 129 sp.add_argument(
117 130 "--sign-timestamp-url",
118 131 help="URL of timestamp server to use for signing",
119 132 )
120 133 sp.add_argument("--version", help="Version string to use")
121 134 sp.add_argument(
122 135 "--extra-packages-script",
123 136 help=(
124 137 "Script to execute to include extra packages in " "py2exe binary."
125 138 ),
126 139 )
127 140 sp.add_argument(
128 141 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
129 142 )
130 143 sp.add_argument(
131 144 "--extra-features",
132 145 help=(
133 146 "CSV of extra feature names to include "
134 147 "in the installer from the extra wxs files"
135 148 ),
136 149 )
137 150 sp.set_defaults(func=build_wix)
138 151
139 152 return parser
140 153
141 154
142 155 def main():
143 156 parser = get_parser()
144 157 args = parser.parse_args()
145 158
146 159 if not hasattr(args, "func"):
147 160 parser.print_help()
148 161 return
149 162
150 163 kwargs = dict(vars(args))
151 164 del kwargs["func"]
152 165
153 166 args.func(**kwargs)
@@ -1,197 +1,227
1 1 # inno.py - Inno Setup functionality.
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import os
11 11 import pathlib
12 12 import shutil
13 13 import subprocess
14 14
15 15 import jinja2
16 16
17 17 from .py2exe import (
18 18 build_py2exe,
19 19 stage_install,
20 20 )
21 from .pyoxidizer import run_pyoxidizer
21 22 from .util import (
22 find_vc_runtime_files,
23 find_legacy_vc_runtime_files,
23 24 normalize_windows_version,
24 25 process_install_rules,
25 26 read_version_py,
26 27 )
27 28
28 29 EXTRA_PACKAGES = {
29 30 'dulwich',
30 31 'keyring',
31 32 'pygments',
32 33 'win32ctypes',
33 34 }
34 35
35 36 EXTRA_INSTALL_RULES = [
36 37 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
37 38 ]
38 39
39 40 PACKAGE_FILES_METADATA = {
40 41 'ReadMe.html': 'Flags: isreadme',
41 42 }
42 43
43 44
44 def build(
45 def build_with_py2exe(
45 46 source_dir: pathlib.Path,
46 47 build_dir: pathlib.Path,
47 48 python_exe: pathlib.Path,
48 49 iscc_exe: pathlib.Path,
49 50 version=None,
50 51 ):
51 """Build the Inno installer.
52 """Build the Inno installer using py2exe.
52 53
53 54 Build files will be placed in ``build_dir``.
54 55
55 56 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
56 57 for finding the Python 2.7 toolchain. So, we require the environment
57 58 to already be configured with an active toolchain.
58 59 """
59 60 if not iscc_exe.exists():
60 61 raise Exception('%s does not exist' % iscc_exe)
61 62
62 63 vc_x64 = r'\x64' in os.environ.get('LIB', '')
63 64 arch = 'x64' if vc_x64 else 'x86'
64 65 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
65 66 staging_dir = inno_build_dir / 'stage'
66 67
67 68 requirements_txt = (
68 69 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
69 70 )
70 71
71 72 inno_build_dir.mkdir(parents=True, exist_ok=True)
72 73
73 74 build_py2exe(
74 75 source_dir,
75 76 build_dir,
76 77 python_exe,
77 78 'inno',
78 79 requirements_txt,
79 80 extra_packages=EXTRA_PACKAGES,
80 81 )
81 82
82 83 # Purge the staging directory for every build so packaging is
83 84 # pristine.
84 85 if staging_dir.exists():
85 86 print('purging %s' % staging_dir)
86 87 shutil.rmtree(staging_dir)
87 88
88 89 # Now assemble all the packaged files into the staging directory.
89 90 stage_install(source_dir, staging_dir)
90 91
91 92 # We also install some extra files.
92 93 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
93 94
94 95 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
95 for f in find_vc_runtime_files(vc_x64):
96 for f in find_legacy_vc_runtime_files(vc_x64):
96 97 if f.name.endswith('.manifest'):
97 98 basename = 'Microsoft.VC90.CRT.manifest'
98 99 else:
99 100 basename = f.name
100 101
101 102 dest_path = staging_dir / basename
102 103
103 104 print('copying %s to %s' % (f, dest_path))
104 105 shutil.copyfile(f, dest_path)
105 106
106 107 build_installer(
107 108 source_dir,
108 109 inno_build_dir,
109 110 staging_dir,
110 111 iscc_exe,
111 112 version,
112 113 arch="x64" if vc_x64 else None,
113 114 )
114 115
115 116
117 def build_with_pyoxidizer(
118 source_dir: pathlib.Path,
119 build_dir: pathlib.Path,
120 target_triple: str,
121 iscc_exe: pathlib.Path,
122 version=None,
123 ):
124 """Build the Inno installer using PyOxidizer."""
125 if not iscc_exe.exists():
126 raise Exception("%s does not exist" % iscc_exe)
127
128 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
129 staging_dir = inno_build_dir / "stage"
130
131 inno_build_dir.mkdir(parents=True, exist_ok=True)
132 run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
133
134 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
135
136 build_installer(
137 source_dir,
138 inno_build_dir,
139 staging_dir,
140 iscc_exe,
141 version,
142 arch="x64" if "x86_64" in target_triple else None,
143 )
144
145
116 146 def build_installer(
117 147 source_dir: pathlib.Path,
118 148 inno_build_dir: pathlib.Path,
119 149 staging_dir: pathlib.Path,
120 150 iscc_exe: pathlib.Path,
121 151 version,
122 152 arch=None,
123 153 ):
124 154 """Build an Inno installer from staged Mercurial files.
125 155
126 156 This function is agnostic about how to build Mercurial. It just
127 157 cares that Mercurial files are in ``staging_dir``.
128 158 """
129 159 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
130 160
131 161 # The final package layout is simply a mirror of the staging directory.
132 162 package_files = []
133 163 for root, dirs, files in os.walk(staging_dir):
134 164 dirs.sort()
135 165
136 166 root = pathlib.Path(root)
137 167
138 168 for f in sorted(files):
139 169 full = root / f
140 170 rel = full.relative_to(staging_dir)
141 171 if str(rel.parent) == '.':
142 172 dest_dir = '{app}'
143 173 else:
144 174 dest_dir = '{app}\\%s' % rel.parent
145 175
146 176 package_files.append(
147 177 {
148 178 'source': rel,
149 179 'dest_dir': dest_dir,
150 180 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
151 181 }
152 182 )
153 183
154 184 print('creating installer')
155 185
156 186 # Install Inno files by rendering a template.
157 187 jinja_env = jinja2.Environment(
158 188 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
159 189 # Need to change these to prevent conflict with Inno Setup.
160 190 comment_start_string='{##',
161 191 comment_end_string='##}',
162 192 )
163 193
164 194 try:
165 195 template = jinja_env.get_template('mercurial.iss')
166 196 except jinja2.TemplateSyntaxError as e:
167 197 raise Exception(
168 198 'template syntax error at %s:%d: %s'
169 199 % (e.name, e.lineno, e.message,)
170 200 )
171 201
172 202 content = template.render(package_files=package_files)
173 203
174 204 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
175 205 fh.write(content)
176 206
177 207 # Copy additional files used by Inno.
178 208 for p in ('mercurial.ico', 'postinstall.txt'):
179 209 shutil.copyfile(
180 210 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
181 211 )
182 212
183 213 args = [str(iscc_exe)]
184 214
185 215 if arch:
186 216 args.append('/dARCH=%s' % arch)
187 217
188 218 if not version:
189 219 version = read_version_py(source_dir)
190 220
191 221 args.append('/dVERSION=%s' % version)
192 222 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
193 223
194 224 args.append('/Odist')
195 225 args.append(str(inno_build_dir / 'mercurial.iss'))
196 226
197 227 subprocess.run(args, cwd=str(source_dir), check=True)
@@ -1,286 +1,338
1 1 # util.py - Common packaging utility code.
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import distutils.version
11 11 import getpass
12 12 import glob
13 13 import os
14 14 import pathlib
15 15 import re
16 16 import shutil
17 17 import subprocess
18 18 import tarfile
19 19 import zipfile
20 20
21 21
22 22 def extract_tar_to_directory(source: pathlib.Path, dest: pathlib.Path):
23 23 with tarfile.open(source, 'r') as tf:
24 24 tf.extractall(dest)
25 25
26 26
27 27 def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path):
28 28 with zipfile.ZipFile(source, 'r') as zf:
29 29 zf.extractall(dest)
30 30
31 31
32 def find_vc_runtime_files(x64=False):
32 def find_vc_runtime_dll(x64=False):
33 """Finds Visual C++ Runtime DLL to include in distribution."""
34 # We invoke vswhere to find the latest Visual Studio install.
35 vswhere = (
36 pathlib.Path(os.environ["ProgramFiles(x86)"])
37 / "Microsoft Visual Studio"
38 / "Installer"
39 / "vswhere.exe"
40 )
41
42 if not vswhere.exists():
43 raise Exception(
44 "could not find vswhere.exe: %s does not exist" % vswhere
45 )
46
47 args = [
48 str(vswhere),
49 # -products * is necessary to return results from Build Tools
50 # (as opposed to full IDE installs).
51 "-products",
52 "*",
53 "-requires",
54 "Microsoft.VisualCpp.Redist.14.Latest",
55 "-latest",
56 "-property",
57 "installationPath",
58 ]
59
60 vs_install_path = pathlib.Path(
61 os.fsdecode(subprocess.check_output(args).strip())
62 )
63
64 # This just gets us a path like
65 # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
66 # Actually vcruntime140.dll is under a path like:
67 # VC\Redist\MSVC\<version>\<arch>\Microsoft.VC14<X>.CRT\vcruntime140.dll.
68
69 arch = "x64" if x64 else "x86"
70
71 search_glob = (
72 r"%s\VC\Redist\MSVC\*\%s\Microsoft.VC14*.CRT\vcruntime140.dll"
73 % (vs_install_path, arch)
74 )
75
76 candidates = glob.glob(search_glob, recursive=True)
77
78 for candidate in reversed(candidates):
79 return pathlib.Path(candidate)
80
81 raise Exception("could not find vcruntime140.dll")
82
83
84 def find_legacy_vc_runtime_files(x64=False):
33 85 """Finds Visual C++ Runtime DLLs to include in distribution."""
34 86 winsxs = pathlib.Path(os.environ['SYSTEMROOT']) / 'WinSxS'
35 87
36 88 prefix = 'amd64' if x64 else 'x86'
37 89
38 90 candidates = sorted(
39 91 p
40 92 for p in os.listdir(winsxs)
41 93 if p.lower().startswith('%s_microsoft.vc90.crt_' % prefix)
42 94 )
43 95
44 96 for p in candidates:
45 97 print('found candidate VC runtime: %s' % p)
46 98
47 99 # Take the newest version.
48 100 version = candidates[-1]
49 101
50 102 d = winsxs / version
51 103
52 104 return [
53 105 d / 'msvcm90.dll',
54 106 d / 'msvcp90.dll',
55 107 d / 'msvcr90.dll',
56 108 winsxs / 'Manifests' / ('%s.manifest' % version),
57 109 ]
58 110
59 111
60 112 def windows_10_sdk_info():
61 113 """Resolves information about the Windows 10 SDK."""
62 114
63 115 base = pathlib.Path(os.environ['ProgramFiles(x86)']) / 'Windows Kits' / '10'
64 116
65 117 if not base.is_dir():
66 118 raise Exception('unable to find Windows 10 SDK at %s' % base)
67 119
68 120 # Find the latest version.
69 121 bin_base = base / 'bin'
70 122
71 123 versions = [v for v in os.listdir(bin_base) if v.startswith('10.')]
72 124 version = sorted(versions, reverse=True)[0]
73 125
74 126 bin_version = bin_base / version
75 127
76 128 return {
77 129 'root': base,
78 130 'version': version,
79 131 'bin_root': bin_version,
80 132 'bin_x86': bin_version / 'x86',
81 133 'bin_x64': bin_version / 'x64',
82 134 }
83 135
84 136
85 137 def normalize_windows_version(version):
86 138 """Normalize Mercurial version string so WiX/Inno accepts it.
87 139
88 140 Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's
89 141 requirements.
90 142
91 143 We normalize RC version or the commit count to a 4th version component.
92 144 We store this in the 4th component because ``A.B.C`` releases do occur
93 145 and we want an e.g. ``5.3rc0`` version to be semantically less than a
94 146 ``5.3.1rc2`` version. This requires always reserving the 3rd version
95 147 component for the point release and the ``X.YrcN`` release is always
96 148 point release 0.
97 149
98 150 In the case of an RC and presence of ``+`` suffix data, we can't use both
99 151 because the version format is limited to 4 components. We choose to use
100 152 RC and throw away the commit count in the suffix. This means we could
101 153 produce multiple installers with the same normalized version string.
102 154
103 155 >>> normalize_windows_version("5.3")
104 156 '5.3.0'
105 157
106 158 >>> normalize_windows_version("5.3rc0")
107 159 '5.3.0.0'
108 160
109 161 >>> normalize_windows_version("5.3rc1")
110 162 '5.3.0.1'
111 163
112 164 >>> normalize_windows_version("5.3rc1+2-abcdef")
113 165 '5.3.0.1'
114 166
115 167 >>> normalize_windows_version("5.3+2-abcdef")
116 168 '5.3.0.2'
117 169 """
118 170 if '+' in version:
119 171 version, extra = version.split('+', 1)
120 172 else:
121 173 extra = None
122 174
123 175 # 4.9rc0
124 176 if version[:-1].endswith('rc'):
125 177 rc = int(version[-1:])
126 178 version = version[:-3]
127 179 else:
128 180 rc = None
129 181
130 182 # Ensure we have at least X.Y version components.
131 183 versions = [int(v) for v in version.split('.')]
132 184 while len(versions) < 3:
133 185 versions.append(0)
134 186
135 187 if len(versions) < 4:
136 188 if rc is not None:
137 189 versions.append(rc)
138 190 elif extra:
139 191 # <commit count>-<hash>+<date>
140 192 versions.append(int(extra.split('-')[0]))
141 193
142 194 return '.'.join('%d' % x for x in versions[0:4])
143 195
144 196
145 197 def find_signtool():
146 198 """Find signtool.exe from the Windows SDK."""
147 199 sdk = windows_10_sdk_info()
148 200
149 201 for key in ('bin_x64', 'bin_x86'):
150 202 p = sdk[key] / 'signtool.exe'
151 203
152 204 if p.exists():
153 205 return p
154 206
155 207 raise Exception('could not find signtool.exe in Windows 10 SDK')
156 208
157 209
158 210 def sign_with_signtool(
159 211 file_path,
160 212 description,
161 213 subject_name=None,
162 214 cert_path=None,
163 215 cert_password=None,
164 216 timestamp_url=None,
165 217 ):
166 218 """Digitally sign a file with signtool.exe.
167 219
168 220 ``file_path`` is file to sign.
169 221 ``description`` is text that goes in the signature.
170 222
171 223 The signing certificate can be specified by ``cert_path`` or
172 224 ``subject_name``. These correspond to the ``/f`` and ``/n`` arguments
173 225 to signtool.exe, respectively.
174 226
175 227 The certificate password can be specified via ``cert_password``. If
176 228 not provided, you will be prompted for the password.
177 229
178 230 ``timestamp_url`` is the URL of a RFC 3161 timestamp server (``/tr``
179 231 argument to signtool.exe).
180 232 """
181 233 if cert_path and subject_name:
182 234 raise ValueError('cannot specify both cert_path and subject_name')
183 235
184 236 while cert_path and not cert_password:
185 237 cert_password = getpass.getpass('password for %s: ' % cert_path)
186 238
187 239 args = [
188 240 str(find_signtool()),
189 241 'sign',
190 242 '/v',
191 243 '/fd',
192 244 'sha256',
193 245 '/d',
194 246 description,
195 247 ]
196 248
197 249 if cert_path:
198 250 args.extend(['/f', str(cert_path), '/p', cert_password])
199 251 elif subject_name:
200 252 args.extend(['/n', subject_name])
201 253
202 254 if timestamp_url:
203 255 args.extend(['/tr', timestamp_url, '/td', 'sha256'])
204 256
205 257 args.append(str(file_path))
206 258
207 259 print('signing %s' % file_path)
208 260 subprocess.run(args, check=True)
209 261
210 262
211 263 PRINT_PYTHON_INFO = '''
212 264 import platform; print("%s:%s" % (platform.architecture()[0], platform.python_version()))
213 265 '''.strip()
214 266
215 267
216 268 def python_exe_info(python_exe: pathlib.Path):
217 269 """Obtain information about a Python executable."""
218 270
219 271 res = subprocess.check_output([str(python_exe), '-c', PRINT_PYTHON_INFO])
220 272
221 273 arch, version = res.decode('utf-8').split(':')
222 274
223 275 version = distutils.version.LooseVersion(version)
224 276
225 277 return {
226 278 'arch': arch,
227 279 'version': version,
228 280 'py3': version >= distutils.version.LooseVersion('3'),
229 281 }
230 282
231 283
232 284 def process_install_rules(
233 285 rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path
234 286 ):
235 287 for source, dest in rules:
236 288 if '*' in source:
237 289 if not dest.endswith('/'):
238 290 raise ValueError('destination must end in / when globbing')
239 291
240 292 # We strip off the source path component before the first glob
241 293 # character to construct the relative install path.
242 294 prefix_end_index = source[: source.index('*')].rindex('/')
243 295 relative_prefix = source_dir / source[0:prefix_end_index]
244 296
245 297 for res in glob.glob(str(source_dir / source), recursive=True):
246 298 source_path = pathlib.Path(res)
247 299
248 300 if source_path.is_dir():
249 301 continue
250 302
251 303 rel_path = source_path.relative_to(relative_prefix)
252 304
253 305 dest_path = dest_dir / dest[:-1] / rel_path
254 306
255 307 dest_path.parent.mkdir(parents=True, exist_ok=True)
256 308 print('copying %s to %s' % (source_path, dest_path))
257 309 shutil.copy(source_path, dest_path)
258 310
259 311 # Simple file case.
260 312 else:
261 313 source_path = pathlib.Path(source)
262 314
263 315 if dest.endswith('/'):
264 316 dest_path = pathlib.Path(dest) / source_path.name
265 317 else:
266 318 dest_path = pathlib.Path(dest)
267 319
268 320 full_source_path = source_dir / source_path
269 321 full_dest_path = dest_dir / dest_path
270 322
271 323 full_dest_path.parent.mkdir(parents=True, exist_ok=True)
272 324 shutil.copy(full_source_path, full_dest_path)
273 325 print('copying %s to %s' % (full_source_path, full_dest_path))
274 326
275 327
276 328 def read_version_py(source_dir):
277 329 """Read the mercurial/__version__.py file to resolve the version string."""
278 330 p = source_dir / 'mercurial' / '__version__.py'
279 331
280 332 with p.open('r', encoding='utf-8') as fh:
281 333 m = re.search('version = b"([^"]+)"', fh.read(), re.MULTILINE)
282 334
283 335 if not m:
284 336 raise Exception('could not parse %s' % p)
285 337
286 338 return m.group(1)
@@ -1,59 +1,104
1 1 ROOT = CWD + "/../.."
2 2
3 def make_exe():
4 dist = default_python_distribution()
3 # Code to run in Python interpreter.
4 RUN_CODE = "import hgdemandimport; hgdemandimport.enable(); from mercurial import dispatch; dispatch.run()"
5
6
7 set_build_path(ROOT + "/build/pyoxidizer")
8
5 9
6 code = "import hgdemandimport; hgdemandimport.enable(); from mercurial import dispatch; dispatch.run()"
10 def make_distribution():
11 return default_python_distribution()
12
7 13
14 def make_distribution_windows():
15 return default_python_distribution(flavor="standalone_dynamic")
16
17
18 def make_exe(dist):
8 19 config = PythonInterpreterConfig(
9 20 raw_allocator = "system",
10 run_eval = code,
21 run_eval = RUN_CODE,
11 22 # We want to let the user load extensions from the file system
12 23 filesystem_importer = True,
13 24 # We need this to make resourceutil happy, since it looks for sys.frozen.
14 25 sys_frozen = True,
15 26 legacy_windows_stdio = True,
16 27 )
17 28
18 29 exe = dist.to_python_executable(
19 30 name = "hg",
20 31 resources_policy = "prefer-in-memory-fallback-filesystem-relative:lib",
21 32 config = config,
22 33 # Extension may depend on any Python functionality. Include all
23 34 # extensions.
24 35 extension_module_filter = "all",
25 36 )
26 37
27 exe.add_python_resources(dist.pip_install([ROOT]))
38 # Add Mercurial to resources.
39 for resource in dist.pip_install(["--verbose", ROOT]):
40 # This is a bit wonky and worth explaining.
41 #
42 # Various parts of Mercurial don't yet support loading package
43 # resources via the ResourceReader interface. Or, not having
44 # file-based resources would be too inconvenient for users.
45 #
46 # So, for package resources, we package them both in the
47 # filesystem as well as in memory. If both are defined,
48 # PyOxidizer will prefer the in-memory location. So even
49 # if the filesystem file isn't packaged in the location
50 # specified here, we should never encounter an errors as the
51 # resource will always be available in memory.
52 if type(resource) == "PythonPackageResource":
53 exe.add_filesystem_relative_python_resource(".", resource)
54 exe.add_in_memory_python_resource(resource)
55 else:
56 exe.add_python_resource(resource)
57
58 # On Windows, we install extra packages for convenience.
59 if "windows" in BUILD_TARGET_TRIPLE:
60 exe.add_python_resources(
61 dist.pip_install(["-r", ROOT + "/contrib/packaging/requirements_win32.txt"])
62 )
28 63
29 64 return exe
30 65
31 def make_install(exe):
66
67 def make_manifest(dist, exe):
32 68 m = FileManifest()
33
34 # `hg` goes in root directory.
35 69 m.add_python_resource(".", exe)
36 70
37 templates = glob(
38 include = [ROOT + "/mercurial/templates/**/*"],
39 strip_prefix = ROOT + "/mercurial/",
40 )
41 m.add_manifest(templates)
71 return m
42 72
43 return m
44 73
45 74 def make_embedded_resources(exe):
46 75 return exe.to_embedded_resources()
47 76
48 register_target("exe", make_exe)
49 register_target("app", make_install, depends = ["exe"], default = True)
50 register_target("embedded", make_embedded_resources, depends = ["exe"], default_build_script = True)
77
78 register_target("distribution_posix", make_distribution)
79 register_target("distribution_windows", make_distribution_windows)
80
81 register_target("exe_posix", make_exe, depends = ["distribution_posix"])
82 register_target("exe_windows", make_exe, depends = ["distribution_windows"])
83
84 register_target(
85 "app_posix",
86 make_manifest,
87 depends = ["distribution_posix", "exe_posix"],
88 default = "windows" not in BUILD_TARGET_TRIPLE,
89 )
90 register_target(
91 "app_windows",
92 make_manifest,
93 depends = ["distribution_windows", "exe_windows"],
94 default = "windows" in BUILD_TARGET_TRIPLE,
95 )
96
51 97 resolve_targets()
52 98
53 99 # END OF COMMON USER-ADJUSTED SETTINGS.
54 100 #
55 101 # Everything below this is typically managed by PyOxidizer and doesn't need
56 102 # to be updated by people.
57 103
58 PYOXIDIZER_VERSION = "0.7.0-pre"
59 PYOXIDIZER_COMMIT = "c772a1379c3026314eda1c8ea244b86c0658951d"
104 PYOXIDIZER_VERSION = "0.7.0"
@@ -1,93 +1,94
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4 $ check_code="$TESTDIR"/../contrib/check-code.py
5 5 $ cd "$TESTDIR"/..
6 6
7 7 New errors are not allowed. Warnings are strongly discouraged.
8 8 (The writing "no-che?k-code" is for not skipping this file when checking.)
9 9
10 10 $ testrepohg locate \
11 11 > -X contrib/python-zstandard \
12 12 > -X hgext/fsmonitor/pywatchman \
13 13 > -X mercurial/thirdparty \
14 14 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
15 15 Skipping contrib/automation/hgautomation/__init__.py it has no-che?k-code (glob)
16 16 Skipping contrib/automation/hgautomation/aws.py it has no-che?k-code (glob)
17 17 Skipping contrib/automation/hgautomation/cli.py it has no-che?k-code (glob)
18 18 Skipping contrib/automation/hgautomation/linux.py it has no-che?k-code (glob)
19 19 Skipping contrib/automation/hgautomation/pypi.py it has no-che?k-code (glob)
20 20 Skipping contrib/automation/hgautomation/ssh.py it has no-che?k-code (glob)
21 21 Skipping contrib/automation/hgautomation/try_server.py it has no-che?k-code (glob)
22 22 Skipping contrib/automation/hgautomation/windows.py it has no-che?k-code (glob)
23 23 Skipping contrib/automation/hgautomation/winrm.py it has no-che?k-code (glob)
24 24 Skipping contrib/fuzz/FuzzedDataProvider.h it has no-che?k-code (glob)
25 25 Skipping contrib/fuzz/standalone_fuzz_target_runner.cc it has no-che?k-code (glob)
26 26 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
27 27 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
28 28 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
29 29 Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/hgpackaging/pyoxidizer.py it has no-che?k-code (glob)
30 31 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
31 32 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
32 33 Skipping i18n/polib.py it has no-che?k-code (glob)
33 34 Skipping mercurial/statprof.py it has no-che?k-code (glob)
34 35 Skipping tests/badserverext.py it has no-che?k-code (glob)
35 36
36 37 @commands in debugcommands.py should be in alphabetical order.
37 38
38 39 >>> import re
39 40 >>> commands = []
40 41 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
41 42 ... for line in fh:
42 43 ... m = re.match(br"^@command\('([a-z]+)", line)
43 44 ... if m:
44 45 ... commands.append(m.group(1))
45 46 >>> scommands = list(sorted(commands))
46 47 >>> for i, command in enumerate(scommands):
47 48 ... if command != commands[i]:
48 49 ... print('commands in debugcommands.py not sorted; first differing '
49 50 ... 'command is %s; expected %s' % (commands[i], command))
50 51 ... break
51 52
52 53 Prevent adding new files in the root directory accidentally.
53 54
54 55 $ testrepohg files 'glob:*'
55 56 .arcconfig
56 57 .clang-format
57 58 .editorconfig
58 59 .hgignore
59 60 .hgsigs
60 61 .hgtags
61 62 .jshintrc
62 63 CONTRIBUTING
63 64 CONTRIBUTORS
64 65 COPYING
65 66 Makefile
66 67 README.rst
67 68 black.toml
68 69 hg
69 70 hgeditor
70 71 hgweb.cgi
71 72 setup.py
72 73
73 74 Prevent adding modules which could be shadowed by ancient .so/.dylib.
74 75
75 76 $ testrepohg files \
76 77 > mercurial/base85.py \
77 78 > mercurial/bdiff.py \
78 79 > mercurial/diffhelpers.py \
79 80 > mercurial/mpatch.py \
80 81 > mercurial/osutil.py \
81 82 > mercurial/parsers.py \
82 83 > mercurial/zstd.py
83 84 [1]
84 85
85 86 Keep python3 tests sorted:
86 87 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
87 88 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
88 89
89 90 Keep Windows line endings in check
90 91
91 92 $ hg files 'set:eol(dos)'
92 93 contrib/win32/hg.bat
93 94 contrib/win32/mercurial.ini
General Comments 0
You need to be logged in to leave comments. Login now