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