##// END OF EJS Templates
packaging: add command line flag to add extra vars to pyoxidizer...
Augie Fackler -
r48444:be37bb8d default
parent child Browse files
Show More
@@ -1,187 +1,194 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=None, python=None, iscc=None, version=None):
24 if not pyoxidizer_target and not python:
24 if not pyoxidizer_target and not python:
25 raise Exception("--python required unless building with PyOxidizer")
25 raise Exception("--python required unless building with PyOxidizer")
26
26
27 if python and not os.path.isabs(python):
27 if python and not os.path.isabs(python):
28 raise Exception("--python arg must be an absolute path")
28 raise Exception("--python arg must be an absolute path")
29
29
30 if iscc:
30 if iscc:
31 iscc = pathlib.Path(iscc)
31 iscc = pathlib.Path(iscc)
32 else:
32 else:
33 iscc = (
33 iscc = (
34 pathlib.Path(os.environ["ProgramFiles(x86)"])
34 pathlib.Path(os.environ["ProgramFiles(x86)"])
35 / "Inno Setup 5"
35 / "Inno Setup 5"
36 / "ISCC.exe"
36 / "ISCC.exe"
37 )
37 )
38
38
39 build_dir = SOURCE_DIR / "build"
39 build_dir = SOURCE_DIR / "build"
40
40
41 if pyoxidizer_target:
41 if pyoxidizer_target:
42 inno.build_with_pyoxidizer(
42 inno.build_with_pyoxidizer(
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
44 )
44 )
45 else:
45 else:
46 inno.build_with_py2exe(
46 inno.build_with_py2exe(
47 SOURCE_DIR,
47 SOURCE_DIR,
48 build_dir,
48 build_dir,
49 pathlib.Path(python),
49 pathlib.Path(python),
50 iscc,
50 iscc,
51 version=version,
51 version=version,
52 )
52 )
53
53
54
54
55 def build_wix(
55 def build_wix(
56 name=None,
56 name=None,
57 pyoxidizer_target=None,
57 pyoxidizer_target=None,
58 python=None,
58 python=None,
59 version=None,
59 version=None,
60 sign_sn=None,
60 sign_sn=None,
61 sign_cert=None,
61 sign_cert=None,
62 sign_password=None,
62 sign_password=None,
63 sign_timestamp_url=None,
63 sign_timestamp_url=None,
64 extra_packages_script=None,
64 extra_packages_script=None,
65 extra_wxs=None,
65 extra_wxs=None,
66 extra_features=None,
66 extra_features=None,
67 extra_pyoxidizer_vars=None,
67 ):
68 ):
68 if not pyoxidizer_target and not python:
69 if not pyoxidizer_target and not python:
69 raise Exception("--python required unless building with PyOxidizer")
70 raise Exception("--python required unless building with PyOxidizer")
70
71
71 if python and not os.path.isabs(python):
72 if python and not os.path.isabs(python):
72 raise Exception("--python arg must be an absolute path")
73 raise Exception("--python arg must be an absolute path")
73
74
74 kwargs = {
75 kwargs = {
75 "source_dir": SOURCE_DIR,
76 "source_dir": SOURCE_DIR,
76 "version": version,
77 "version": version,
77 }
78 }
78
79
79 if pyoxidizer_target:
80 if pyoxidizer_target:
80 fn = wix.build_installer_pyoxidizer
81 fn = wix.build_installer_pyoxidizer
81 kwargs["target_triple"] = pyoxidizer_target
82 kwargs["target_triple"] = pyoxidizer_target
82 else:
83 else:
83 fn = wix.build_installer_py2exe
84 fn = wix.build_installer_py2exe
84 kwargs["python_exe"] = pathlib.Path(python)
85 kwargs["python_exe"] = pathlib.Path(python)
85
86
86 if extra_packages_script:
87 if extra_packages_script:
87 if pyoxidizer_target:
88 if pyoxidizer_target:
88 raise Exception(
89 raise Exception(
89 "pyoxidizer does not support --extra-packages-script"
90 "pyoxidizer does not support --extra-packages-script"
90 )
91 )
91 kwargs["extra_packages_script"] = extra_packages_script
92 kwargs["extra_packages_script"] = extra_packages_script
92 if extra_wxs:
93 if extra_wxs:
93 kwargs["extra_wxs"] = dict(
94 kwargs["extra_wxs"] = dict(
94 thing.split("=") for thing in extra_wxs.split(",")
95 thing.split("=") for thing in extra_wxs.split(",")
95 )
96 )
96 if extra_features:
97 if extra_features:
97 kwargs["extra_features"] = extra_features.split(",")
98 kwargs["extra_features"] = extra_features.split(",")
98
99
99 if sign_sn or sign_cert:
100 if sign_sn or sign_cert:
100 kwargs["signing_info"] = {
101 kwargs["signing_info"] = {
101 "name": name,
102 "name": name,
102 "subject_name": sign_sn,
103 "subject_name": sign_sn,
103 "cert_path": sign_cert,
104 "cert_path": sign_cert,
104 "cert_password": sign_password,
105 "cert_password": sign_password,
105 "timestamp_url": sign_timestamp_url,
106 "timestamp_url": sign_timestamp_url,
106 }
107 }
107
108
108 fn(**kwargs)
109 fn(**kwargs, extra_pyoxidizer_vars=extra_pyoxidizer_vars)
109
110
110
111
111 def get_parser():
112 def get_parser():
112 parser = argparse.ArgumentParser()
113 parser = argparse.ArgumentParser()
113
114
114 subparsers = parser.add_subparsers()
115 subparsers = parser.add_subparsers()
115
116
116 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
117 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
117 sp.add_argument(
118 sp.add_argument(
118 "--pyoxidizer-target",
119 "--pyoxidizer-target",
119 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
120 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
120 help="Build with PyOxidizer targeting this host triple",
121 help="Build with PyOxidizer targeting this host triple",
121 )
122 )
122 sp.add_argument("--python", help="path to python.exe to use")
123 sp.add_argument("--python", help="path to python.exe to use")
123 sp.add_argument("--iscc", help="path to iscc.exe to use")
124 sp.add_argument("--iscc", help="path to iscc.exe to use")
124 sp.add_argument(
125 sp.add_argument(
125 "--version",
126 "--version",
126 help="Mercurial version string to use "
127 help="Mercurial version string to use "
127 "(detected from __version__.py if not defined",
128 "(detected from __version__.py if not defined",
128 )
129 )
129 sp.set_defaults(func=build_inno)
130 sp.set_defaults(func=build_inno)
130
131
131 sp = subparsers.add_parser(
132 sp = subparsers.add_parser(
132 "wix", help="Build Windows installer with WiX Toolset"
133 "wix", help="Build Windows installer with WiX Toolset"
133 )
134 )
134 sp.add_argument("--name", help="Application name", default="Mercurial")
135 sp.add_argument("--name", help="Application name", default="Mercurial")
135 sp.add_argument(
136 sp.add_argument(
136 "--pyoxidizer-target",
137 "--pyoxidizer-target",
137 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
138 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
138 help="Build with PyOxidizer targeting this host triple",
139 help="Build with PyOxidizer targeting this host triple",
139 )
140 )
140 sp.add_argument("--python", help="Path to Python executable to use")
141 sp.add_argument("--python", help="Path to Python executable to use")
141 sp.add_argument(
142 sp.add_argument(
142 "--sign-sn",
143 "--sign-sn",
143 help="Subject name (or fragment thereof) of certificate "
144 help="Subject name (or fragment thereof) of certificate "
144 "to use for signing",
145 "to use for signing",
145 )
146 )
146 sp.add_argument(
147 sp.add_argument(
147 "--sign-cert", help="Path to certificate to use for signing"
148 "--sign-cert", help="Path to certificate to use for signing"
148 )
149 )
149 sp.add_argument("--sign-password", help="Password for signing certificate")
150 sp.add_argument("--sign-password", help="Password for signing certificate")
150 sp.add_argument(
151 sp.add_argument(
151 "--sign-timestamp-url",
152 "--sign-timestamp-url",
152 help="URL of timestamp server to use for signing",
153 help="URL of timestamp server to use for signing",
153 )
154 )
154 sp.add_argument("--version", help="Version string to use")
155 sp.add_argument("--version", help="Version string to use")
155 sp.add_argument(
156 sp.add_argument(
156 "--extra-packages-script",
157 "--extra-packages-script",
157 help=(
158 help=(
158 "Script to execute to include extra packages in " "py2exe binary."
159 "Script to execute to include extra packages in " "py2exe binary."
159 ),
160 ),
160 )
161 )
161 sp.add_argument(
162 sp.add_argument(
162 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
163 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
163 )
164 )
164 sp.add_argument(
165 sp.add_argument(
165 "--extra-features",
166 "--extra-features",
166 help=(
167 help=(
167 "CSV of extra feature names to include "
168 "CSV of extra feature names to include "
168 "in the installer from the extra wxs files"
169 "in the installer from the extra wxs files"
169 ),
170 ),
170 )
171 )
172
173 sp.add_argument(
174 "--extra-pyoxidizer-vars",
175 help="json map of extra variables to pass to pyoxidizer",
176 )
177
171 sp.set_defaults(func=build_wix)
178 sp.set_defaults(func=build_wix)
172
179
173 return parser
180 return parser
174
181
175
182
176 def main():
183 def main():
177 parser = get_parser()
184 parser = get_parser()
178 args = parser.parse_args()
185 args = parser.parse_args()
179
186
180 if not hasattr(args, "func"):
187 if not hasattr(args, "func"):
181 parser.print_help()
188 parser.print_help()
182 return
189 return
183
190
184 kwargs = dict(vars(args))
191 kwargs = dict(vars(args))
185 del kwargs["func"]
192 del kwargs["func"]
186
193
187 args.func(**kwargs)
194 args.func(**kwargs)
@@ -1,581 +1,586 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
10 import collections
11 import json
11 import os
12 import os
12 import pathlib
13 import pathlib
13 import re
14 import re
14 import shutil
15 import shutil
15 import subprocess
16 import subprocess
16 import typing
17 import typing
17 import uuid
18 import uuid
18 import xml.dom.minidom
19 import xml.dom.minidom
19
20
20 from .downloads import download_entry
21 from .downloads import download_entry
21 from .py2exe import (
22 from .py2exe import (
22 build_py2exe,
23 build_py2exe,
23 stage_install,
24 stage_install,
24 )
25 )
25 from .pyoxidizer import (
26 from .pyoxidizer import (
26 build_docs_html,
27 build_docs_html,
27 create_pyoxidizer_install_layout,
28 create_pyoxidizer_install_layout,
28 run_pyoxidizer,
29 run_pyoxidizer,
29 )
30 )
30 from .util import (
31 from .util import (
31 extract_zip_to_directory,
32 extract_zip_to_directory,
32 normalize_windows_version,
33 normalize_windows_version,
33 process_install_rules,
34 process_install_rules,
34 sign_with_signtool,
35 sign_with_signtool,
35 )
36 )
36
37
37
38
38 EXTRA_PACKAGES = {
39 EXTRA_PACKAGES = {
39 'dulwich',
40 'dulwich',
40 'distutils',
41 'distutils',
41 'keyring',
42 'keyring',
42 'pygments',
43 'pygments',
43 'win32ctypes',
44 'win32ctypes',
44 }
45 }
45
46
46 EXTRA_INCLUDES = {
47 EXTRA_INCLUDES = {
47 '_curses',
48 '_curses',
48 '_curses_panel',
49 '_curses_panel',
49 }
50 }
50
51
51 EXTRA_INSTALL_RULES = [
52 EXTRA_INSTALL_RULES = [
52 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
53 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
53 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
54 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
54 ]
55 ]
55
56
56 STAGING_REMOVE_FILES = [
57 STAGING_REMOVE_FILES = [
57 # We use the RTF variant.
58 # We use the RTF variant.
58 'copying.txt',
59 'copying.txt',
59 ]
60 ]
60
61
61 SHORTCUTS = {
62 SHORTCUTS = {
62 # hg.1.html'
63 # hg.1.html'
63 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
64 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
64 'Name': 'Mercurial Command Reference',
65 'Name': 'Mercurial Command Reference',
65 },
66 },
66 # hgignore.5.html
67 # hgignore.5.html
67 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
68 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
68 'Name': 'Mercurial Ignore Files',
69 'Name': 'Mercurial Ignore Files',
69 },
70 },
70 # hgrc.5.html
71 # hgrc.5.html
71 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
72 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
72 'Name': 'Mercurial Configuration Files',
73 'Name': 'Mercurial Configuration Files',
73 },
74 },
74 }
75 }
75
76
76
77
77 def find_version(source_dir: pathlib.Path):
78 def find_version(source_dir: pathlib.Path):
78 version_py = source_dir / 'mercurial' / '__version__.py'
79 version_py = source_dir / 'mercurial' / '__version__.py'
79
80
80 with version_py.open('r', encoding='utf-8') as fh:
81 with version_py.open('r', encoding='utf-8') as fh:
81 source = fh.read().strip()
82 source = fh.read().strip()
82
83
83 m = re.search('version = b"(.*)"', source)
84 m = re.search('version = b"(.*)"', source)
84 return m.group(1)
85 return m.group(1)
85
86
86
87
87 def ensure_vc90_merge_modules(build_dir):
88 def ensure_vc90_merge_modules(build_dir):
88 x86 = (
89 x86 = (
89 download_entry(
90 download_entry(
90 'vc9-crt-x86-msm',
91 'vc9-crt-x86-msm',
91 build_dir,
92 build_dir,
92 local_name='microsoft.vcxx.crt.x86_msm.msm',
93 local_name='microsoft.vcxx.crt.x86_msm.msm',
93 )[0],
94 )[0],
94 download_entry(
95 download_entry(
95 'vc9-crt-x86-msm-policy',
96 'vc9-crt-x86-msm-policy',
96 build_dir,
97 build_dir,
97 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
98 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
98 )[0],
99 )[0],
99 )
100 )
100
101
101 x64 = (
102 x64 = (
102 download_entry(
103 download_entry(
103 'vc9-crt-x64-msm',
104 'vc9-crt-x64-msm',
104 build_dir,
105 build_dir,
105 local_name='microsoft.vcxx.crt.x64_msm.msm',
106 local_name='microsoft.vcxx.crt.x64_msm.msm',
106 )[0],
107 )[0],
107 download_entry(
108 download_entry(
108 'vc9-crt-x64-msm-policy',
109 'vc9-crt-x64-msm-policy',
109 build_dir,
110 build_dir,
110 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
111 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
111 )[0],
112 )[0],
112 )
113 )
113 return {
114 return {
114 'x86': x86,
115 'x86': x86,
115 'x64': x64,
116 'x64': x64,
116 }
117 }
117
118
118
119
119 def run_candle(wix, cwd, wxs, source_dir, defines=None):
120 def run_candle(wix, cwd, wxs, source_dir, defines=None):
120 args = [
121 args = [
121 str(wix / 'candle.exe'),
122 str(wix / 'candle.exe'),
122 '-nologo',
123 '-nologo',
123 str(wxs),
124 str(wxs),
124 '-dSourceDir=%s' % source_dir,
125 '-dSourceDir=%s' % source_dir,
125 ]
126 ]
126
127
127 if defines:
128 if defines:
128 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
129 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
129
130
130 subprocess.run(args, cwd=str(cwd), check=True)
131 subprocess.run(args, cwd=str(cwd), check=True)
131
132
132
133
133 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
134 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
134 """Create XML string listing every file to be installed."""
135 """Create XML string listing every file to be installed."""
135
136
136 # We derive GUIDs from a deterministic file path identifier.
137 # We derive GUIDs from a deterministic file path identifier.
137 # We shoehorn the name into something that looks like a URL because
138 # We shoehorn the name into something that looks like a URL because
138 # the UUID namespaces are supposed to work that way (even though
139 # the UUID namespaces are supposed to work that way (even though
139 # the input data probably is never validated).
140 # the input data probably is never validated).
140
141
141 doc = xml.dom.minidom.parseString(
142 doc = xml.dom.minidom.parseString(
142 '<?xml version="1.0" encoding="utf-8"?>'
143 '<?xml version="1.0" encoding="utf-8"?>'
143 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
144 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
144 '</Wix>'
145 '</Wix>'
145 )
146 )
146
147
147 # Assemble the install layout by directory. This makes it easier to
148 # Assemble the install layout by directory. This makes it easier to
148 # emit XML, since each directory has separate entities.
149 # emit XML, since each directory has separate entities.
149 manifest = collections.defaultdict(dict)
150 manifest = collections.defaultdict(dict)
150
151
151 for root, dirs, files in os.walk(staging_dir):
152 for root, dirs, files in os.walk(staging_dir):
152 dirs.sort()
153 dirs.sort()
153
154
154 root = pathlib.Path(root)
155 root = pathlib.Path(root)
155 rel_dir = root.relative_to(staging_dir)
156 rel_dir = root.relative_to(staging_dir)
156
157
157 for i in range(len(rel_dir.parts)):
158 for i in range(len(rel_dir.parts)):
158 parent = '/'.join(rel_dir.parts[0 : i + 1])
159 parent = '/'.join(rel_dir.parts[0 : i + 1])
159 manifest.setdefault(parent, {})
160 manifest.setdefault(parent, {})
160
161
161 for f in sorted(files):
162 for f in sorted(files):
162 full = root / f
163 full = root / f
163 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
164 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
164
165
165 component_groups = collections.defaultdict(list)
166 component_groups = collections.defaultdict(list)
166
167
167 # Now emit a <Fragment> for each directory.
168 # Now emit a <Fragment> for each directory.
168 # Each directory is composed of a <DirectoryRef> pointing to its parent
169 # Each directory is composed of a <DirectoryRef> pointing to its parent
169 # and defines child <Directory>'s and a <Component> with all the files.
170 # and defines child <Directory>'s and a <Component> with all the files.
170 for dir_name, entries in sorted(manifest.items()):
171 for dir_name, entries in sorted(manifest.items()):
171 # The directory id is derived from the path. But the root directory
172 # The directory id is derived from the path. But the root directory
172 # is special.
173 # is special.
173 if dir_name == '.':
174 if dir_name == '.':
174 parent_directory_id = 'INSTALLDIR'
175 parent_directory_id = 'INSTALLDIR'
175 else:
176 else:
176 parent_directory_id = 'hg.dir.%s' % dir_name.replace(
177 parent_directory_id = 'hg.dir.%s' % dir_name.replace(
177 '/', '.'
178 '/', '.'
178 ).replace('-', '_')
179 ).replace('-', '_')
179
180
180 fragment = doc.createElement('Fragment')
181 fragment = doc.createElement('Fragment')
181 directory_ref = doc.createElement('DirectoryRef')
182 directory_ref = doc.createElement('DirectoryRef')
182 directory_ref.setAttribute('Id', parent_directory_id)
183 directory_ref.setAttribute('Id', parent_directory_id)
183
184
184 # Add <Directory> entries for immediate children directories.
185 # Add <Directory> entries for immediate children directories.
185 for possible_child in sorted(manifest.keys()):
186 for possible_child in sorted(manifest.keys()):
186 if (
187 if (
187 dir_name == '.'
188 dir_name == '.'
188 and '/' not in possible_child
189 and '/' not in possible_child
189 and possible_child != '.'
190 and possible_child != '.'
190 ):
191 ):
191 child_directory_id = ('hg.dir.%s' % possible_child).replace(
192 child_directory_id = ('hg.dir.%s' % possible_child).replace(
192 '-', '_'
193 '-', '_'
193 )
194 )
194 name = possible_child
195 name = possible_child
195 else:
196 else:
196 if not possible_child.startswith('%s/' % dir_name):
197 if not possible_child.startswith('%s/' % dir_name):
197 continue
198 continue
198 name = possible_child[len(dir_name) + 1 :]
199 name = possible_child[len(dir_name) + 1 :]
199 if '/' in name:
200 if '/' in name:
200 continue
201 continue
201
202
202 child_directory_id = 'hg.dir.%s' % possible_child.replace(
203 child_directory_id = 'hg.dir.%s' % possible_child.replace(
203 '/', '.'
204 '/', '.'
204 ).replace('-', '_')
205 ).replace('-', '_')
205
206
206 directory = doc.createElement('Directory')
207 directory = doc.createElement('Directory')
207 directory.setAttribute('Id', child_directory_id)
208 directory.setAttribute('Id', child_directory_id)
208 directory.setAttribute('Name', name)
209 directory.setAttribute('Name', name)
209 directory_ref.appendChild(directory)
210 directory_ref.appendChild(directory)
210
211
211 # Add <Component>s for files in this directory.
212 # Add <Component>s for files in this directory.
212 for rel, source_path in sorted(entries.items()):
213 for rel, source_path in sorted(entries.items()):
213 if dir_name == '.':
214 if dir_name == '.':
214 full_rel = rel
215 full_rel = rel
215 else:
216 else:
216 full_rel = '%s/%s' % (dir_name, rel)
217 full_rel = '%s/%s' % (dir_name, rel)
217
218
218 component_unique_id = (
219 component_unique_id = (
219 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
220 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
220 % full_rel
221 % full_rel
221 )
222 )
222 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
223 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
223 component_id = 'hg.component.%s' % str(component_guid).replace(
224 component_id = 'hg.component.%s' % str(component_guid).replace(
224 '-', '_'
225 '-', '_'
225 )
226 )
226
227
227 component = doc.createElement('Component')
228 component = doc.createElement('Component')
228
229
229 component.setAttribute('Id', component_id)
230 component.setAttribute('Id', component_id)
230 component.setAttribute('Guid', str(component_guid).upper())
231 component.setAttribute('Guid', str(component_guid).upper())
231 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
232 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
232
233
233 # Assign this component to a top-level group.
234 # Assign this component to a top-level group.
234 if dir_name == '.':
235 if dir_name == '.':
235 component_groups['ROOT'].append(component_id)
236 component_groups['ROOT'].append(component_id)
236 elif '/' in dir_name:
237 elif '/' in dir_name:
237 component_groups[dir_name[0 : dir_name.index('/')]].append(
238 component_groups[dir_name[0 : dir_name.index('/')]].append(
238 component_id
239 component_id
239 )
240 )
240 else:
241 else:
241 component_groups[dir_name].append(component_id)
242 component_groups[dir_name].append(component_id)
242
243
243 unique_id = (
244 unique_id = (
244 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
245 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
245 )
246 )
246 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
247 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
247
248
248 # IDs have length limits. So use GUID to derive them.
249 # IDs have length limits. So use GUID to derive them.
249 file_guid_normalized = str(file_guid).replace('-', '_')
250 file_guid_normalized = str(file_guid).replace('-', '_')
250 file_id = 'hg.file.%s' % file_guid_normalized
251 file_id = 'hg.file.%s' % file_guid_normalized
251
252
252 file_element = doc.createElement('File')
253 file_element = doc.createElement('File')
253 file_element.setAttribute('Id', file_id)
254 file_element.setAttribute('Id', file_id)
254 file_element.setAttribute('Source', str(source_path))
255 file_element.setAttribute('Source', str(source_path))
255 file_element.setAttribute('KeyPath', 'yes')
256 file_element.setAttribute('KeyPath', 'yes')
256 file_element.setAttribute('ReadOnly', 'yes')
257 file_element.setAttribute('ReadOnly', 'yes')
257
258
258 component.appendChild(file_element)
259 component.appendChild(file_element)
259 directory_ref.appendChild(component)
260 directory_ref.appendChild(component)
260
261
261 fragment.appendChild(directory_ref)
262 fragment.appendChild(directory_ref)
262 doc.documentElement.appendChild(fragment)
263 doc.documentElement.appendChild(fragment)
263
264
264 for group, component_ids in sorted(component_groups.items()):
265 for group, component_ids in sorted(component_groups.items()):
265 fragment = doc.createElement('Fragment')
266 fragment = doc.createElement('Fragment')
266 component_group = doc.createElement('ComponentGroup')
267 component_group = doc.createElement('ComponentGroup')
267 component_group.setAttribute('Id', 'hg.group.%s' % group)
268 component_group.setAttribute('Id', 'hg.group.%s' % group)
268
269
269 for component_id in component_ids:
270 for component_id in component_ids:
270 component_ref = doc.createElement('ComponentRef')
271 component_ref = doc.createElement('ComponentRef')
271 component_ref.setAttribute('Id', component_id)
272 component_ref.setAttribute('Id', component_id)
272 component_group.appendChild(component_ref)
273 component_group.appendChild(component_ref)
273
274
274 fragment.appendChild(component_group)
275 fragment.appendChild(component_group)
275 doc.documentElement.appendChild(fragment)
276 doc.documentElement.appendChild(fragment)
276
277
277 # Add <Shortcut> to files that have it defined.
278 # Add <Shortcut> to files that have it defined.
278 for file_id, metadata in sorted(SHORTCUTS.items()):
279 for file_id, metadata in sorted(SHORTCUTS.items()):
279 els = doc.getElementsByTagName('File')
280 els = doc.getElementsByTagName('File')
280 els = [el for el in els if el.getAttribute('Id') == file_id]
281 els = [el for el in els if el.getAttribute('Id') == file_id]
281
282
282 if not els:
283 if not els:
283 raise Exception('could not find File[Id=%s]' % file_id)
284 raise Exception('could not find File[Id=%s]' % file_id)
284
285
285 for el in els:
286 for el in els:
286 shortcut = doc.createElement('Shortcut')
287 shortcut = doc.createElement('Shortcut')
287 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
288 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
288 shortcut.setAttribute('Directory', 'ProgramMenuDir')
289 shortcut.setAttribute('Directory', 'ProgramMenuDir')
289 shortcut.setAttribute('Icon', 'hgIcon.ico')
290 shortcut.setAttribute('Icon', 'hgIcon.ico')
290 shortcut.setAttribute('IconIndex', '0')
291 shortcut.setAttribute('IconIndex', '0')
291 shortcut.setAttribute('Advertise', 'yes')
292 shortcut.setAttribute('Advertise', 'yes')
292 for k, v in sorted(metadata.items()):
293 for k, v in sorted(metadata.items()):
293 shortcut.setAttribute(k, v)
294 shortcut.setAttribute(k, v)
294
295
295 el.appendChild(shortcut)
296 el.appendChild(shortcut)
296
297
297 return doc.toprettyxml()
298 return doc.toprettyxml()
298
299
299
300
300 def build_installer_py2exe(
301 def build_installer_py2exe(
301 source_dir: pathlib.Path,
302 source_dir: pathlib.Path,
302 python_exe: pathlib.Path,
303 python_exe: pathlib.Path,
303 msi_name='mercurial',
304 msi_name='mercurial',
304 version=None,
305 version=None,
305 extra_packages_script=None,
306 extra_packages_script=None,
306 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
307 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
307 extra_features: typing.Optional[typing.List[str]] = None,
308 extra_features: typing.Optional[typing.List[str]] = None,
308 signing_info: typing.Optional[typing.Dict[str, str]] = None,
309 signing_info: typing.Optional[typing.Dict[str, str]] = None,
309 ):
310 ):
310 """Build a WiX MSI installer using py2exe.
311 """Build a WiX MSI installer using py2exe.
311
312
312 ``source_dir`` is the path to the Mercurial source tree to use.
313 ``source_dir`` is the path to the Mercurial source tree to use.
313 ``arch`` is the target architecture. either ``x86`` or ``x64``.
314 ``arch`` is the target architecture. either ``x86`` or ``x64``.
314 ``python_exe`` is the path to the Python executable to use/bundle.
315 ``python_exe`` is the path to the Python executable to use/bundle.
315 ``version`` is the Mercurial version string. If not defined,
316 ``version`` is the Mercurial version string. If not defined,
316 ``mercurial/__version__.py`` will be consulted.
317 ``mercurial/__version__.py`` will be consulted.
317 ``extra_packages_script`` is a command to be run to inject extra packages
318 ``extra_packages_script`` is a command to be run to inject extra packages
318 into the py2exe binary. It should stage packages into the virtualenv and
319 into the py2exe binary. It should stage packages into the virtualenv and
319 print a null byte followed by a newline-separated list of packages that
320 print a null byte followed by a newline-separated list of packages that
320 should be included in the exe.
321 should be included in the exe.
321 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
322 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
322 ``extra_features`` is a list of additional named Features to include in
323 ``extra_features`` is a list of additional named Features to include in
323 the build. These must match Feature names in one of the wxs scripts.
324 the build. These must match Feature names in one of the wxs scripts.
324 """
325 """
325 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
326 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
326
327
327 hg_build_dir = source_dir / 'build'
328 hg_build_dir = source_dir / 'build'
328
329
329 requirements_txt = (
330 requirements_txt = (
330 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
331 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
331 )
332 )
332
333
333 build_py2exe(
334 build_py2exe(
334 source_dir,
335 source_dir,
335 hg_build_dir,
336 hg_build_dir,
336 python_exe,
337 python_exe,
337 'wix',
338 'wix',
338 requirements_txt,
339 requirements_txt,
339 extra_packages=EXTRA_PACKAGES,
340 extra_packages=EXTRA_PACKAGES,
340 extra_packages_script=extra_packages_script,
341 extra_packages_script=extra_packages_script,
341 extra_includes=EXTRA_INCLUDES,
342 extra_includes=EXTRA_INCLUDES,
342 )
343 )
343
344
344 build_dir = hg_build_dir / ('wix-%s' % arch)
345 build_dir = hg_build_dir / ('wix-%s' % arch)
345 staging_dir = build_dir / 'stage'
346 staging_dir = build_dir / 'stage'
346
347
347 build_dir.mkdir(exist_ok=True)
348 build_dir.mkdir(exist_ok=True)
348
349
349 # Purge the staging directory for every build so packaging is pristine.
350 # Purge the staging directory for every build so packaging is pristine.
350 if staging_dir.exists():
351 if staging_dir.exists():
351 print('purging %s' % staging_dir)
352 print('purging %s' % staging_dir)
352 shutil.rmtree(staging_dir)
353 shutil.rmtree(staging_dir)
353
354
354 stage_install(source_dir, staging_dir, lower_case=True)
355 stage_install(source_dir, staging_dir, lower_case=True)
355
356
356 # We also install some extra files.
357 # We also install some extra files.
357 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
358 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
358
359
359 # And remove some files we don't want.
360 # And remove some files we don't want.
360 for f in STAGING_REMOVE_FILES:
361 for f in STAGING_REMOVE_FILES:
361 p = staging_dir / f
362 p = staging_dir / f
362 if p.exists():
363 if p.exists():
363 print('removing %s' % p)
364 print('removing %s' % p)
364 p.unlink()
365 p.unlink()
365
366
366 return run_wix_packaging(
367 return run_wix_packaging(
367 source_dir,
368 source_dir,
368 build_dir,
369 build_dir,
369 staging_dir,
370 staging_dir,
370 arch,
371 arch,
371 version=version,
372 version=version,
372 python2=True,
373 python2=True,
373 msi_name=msi_name,
374 msi_name=msi_name,
374 suffix="-python2",
375 suffix="-python2",
375 extra_wxs=extra_wxs,
376 extra_wxs=extra_wxs,
376 extra_features=extra_features,
377 extra_features=extra_features,
377 signing_info=signing_info,
378 signing_info=signing_info,
378 )
379 )
379
380
380
381
381 def build_installer_pyoxidizer(
382 def build_installer_pyoxidizer(
382 source_dir: pathlib.Path,
383 source_dir: pathlib.Path,
383 target_triple: str,
384 target_triple: str,
384 msi_name='mercurial',
385 msi_name='mercurial',
385 version=None,
386 version=None,
386 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
387 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
387 extra_features: typing.Optional[typing.List[str]] = None,
388 extra_features: typing.Optional[typing.List[str]] = None,
388 signing_info: typing.Optional[typing.Dict[str, str]] = None,
389 signing_info: typing.Optional[typing.Dict[str, str]] = None,
390 extra_pyoxidizer_vars=None,
389 ):
391 ):
390 """Build a WiX MSI installer using PyOxidizer."""
392 """Build a WiX MSI installer using PyOxidizer."""
391 hg_build_dir = source_dir / "build"
393 hg_build_dir = source_dir / "build"
392 build_dir = hg_build_dir / ("wix-%s" % target_triple)
394 build_dir = hg_build_dir / ("wix-%s" % target_triple)
393
395
394 build_dir.mkdir(parents=True, exist_ok=True)
396 build_dir.mkdir(parents=True, exist_ok=True)
395
397
396 # Need to ensure docs HTML is built because this isn't done as part of
398 # Need to ensure docs HTML is built because this isn't done as part of
397 # `pip install Mercurial`.
399 # `pip install Mercurial`.
398 build_docs_html(source_dir)
400 build_docs_html(source_dir)
399
401
400 build_vars = {}
402 build_vars = {}
401
403
402 if msi_name:
404 if msi_name:
403 build_vars["MSI_NAME"] = msi_name
405 build_vars["MSI_NAME"] = msi_name
404
406
405 if version:
407 if version:
406 build_vars["VERSION"] = version
408 build_vars["VERSION"] = version
407
409
408 if extra_features:
410 if extra_features:
409 build_vars["EXTRA_MSI_FEATURES"] = ";".join(extra_features)
411 build_vars["EXTRA_MSI_FEATURES"] = ";".join(extra_features)
410
412
411 if signing_info:
413 if signing_info:
412 if signing_info["cert_path"]:
414 if signing_info["cert_path"]:
413 build_vars["SIGNING_PFX_PATH"] = signing_info["cert_path"]
415 build_vars["SIGNING_PFX_PATH"] = signing_info["cert_path"]
414 if signing_info["cert_password"]:
416 if signing_info["cert_password"]:
415 build_vars["SIGNING_PFX_PASSWORD"] = signing_info["cert_password"]
417 build_vars["SIGNING_PFX_PASSWORD"] = signing_info["cert_password"]
416 if signing_info["subject_name"]:
418 if signing_info["subject_name"]:
417 build_vars["SIGNING_SUBJECT_NAME"] = signing_info["subject_name"]
419 build_vars["SIGNING_SUBJECT_NAME"] = signing_info["subject_name"]
418 if signing_info["timestamp_url"]:
420 if signing_info["timestamp_url"]:
419 build_vars["TIME_STAMP_SERVER_URL"] = signing_info["timestamp_url"]
421 build_vars["TIME_STAMP_SERVER_URL"] = signing_info["timestamp_url"]
420
422
423 if extra_pyoxidizer_vars:
424 build_vars.update(json.loads(extra_pyoxidizer_vars))
425
421 if extra_wxs:
426 if extra_wxs:
422 raise Exception(
427 raise Exception(
423 "support for extra .wxs files has been temporarily dropped"
428 "support for extra .wxs files has been temporarily dropped"
424 )
429 )
425
430
426 out_dir = run_pyoxidizer(
431 out_dir = run_pyoxidizer(
427 source_dir,
432 source_dir,
428 build_dir,
433 build_dir,
429 target_triple,
434 target_triple,
430 build_vars=build_vars,
435 build_vars=build_vars,
431 target="msi",
436 target="msi",
432 )
437 )
433
438
434 msi_dir = out_dir / "msi"
439 msi_dir = out_dir / "msi"
435 msi_files = [f for f in os.listdir(msi_dir) if f.endswith(".msi")]
440 msi_files = [f for f in os.listdir(msi_dir) if f.endswith(".msi")]
436
441
437 if len(msi_files) != 1:
442 if len(msi_files) != 1:
438 raise Exception("expected exactly 1 .msi file; got %d" % len(msi_files))
443 raise Exception("expected exactly 1 .msi file; got %d" % len(msi_files))
439
444
440 msi_filename = msi_files[0]
445 msi_filename = msi_files[0]
441
446
442 msi_path = msi_dir / msi_filename
447 msi_path = msi_dir / msi_filename
443 dist_path = source_dir / "dist" / msi_filename
448 dist_path = source_dir / "dist" / msi_filename
444
449
445 dist_path.parent.mkdir(parents=True, exist_ok=True)
450 dist_path.parent.mkdir(parents=True, exist_ok=True)
446
451
447 shutil.copyfile(msi_path, dist_path)
452 shutil.copyfile(msi_path, dist_path)
448
453
449 return {
454 return {
450 "msi_path": dist_path,
455 "msi_path": dist_path,
451 }
456 }
452
457
453
458
454 def run_wix_packaging(
459 def run_wix_packaging(
455 source_dir: pathlib.Path,
460 source_dir: pathlib.Path,
456 build_dir: pathlib.Path,
461 build_dir: pathlib.Path,
457 staging_dir: pathlib.Path,
462 staging_dir: pathlib.Path,
458 arch: str,
463 arch: str,
459 version: str,
464 version: str,
460 python2: bool,
465 python2: bool,
461 msi_name: typing.Optional[str] = "mercurial",
466 msi_name: typing.Optional[str] = "mercurial",
462 suffix: str = "",
467 suffix: str = "",
463 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
468 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
464 extra_features: typing.Optional[typing.List[str]] = None,
469 extra_features: typing.Optional[typing.List[str]] = None,
465 signing_info: typing.Optional[typing.Dict[str, str]] = None,
470 signing_info: typing.Optional[typing.Dict[str, str]] = None,
466 ):
471 ):
467 """Invokes WiX to package up a built Mercurial.
472 """Invokes WiX to package up a built Mercurial.
468
473
469 ``signing_info`` is a dict defining properties to facilitate signing the
474 ``signing_info`` is a dict defining properties to facilitate signing the
470 installer. Recognized keys include ``name``, ``subject_name``,
475 installer. Recognized keys include ``name``, ``subject_name``,
471 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
476 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
472 we will sign both the hg.exe and the .msi using the signing credentials
477 we will sign both the hg.exe and the .msi using the signing credentials
473 specified.
478 specified.
474 """
479 """
475
480
476 orig_version = version or find_version(source_dir)
481 orig_version = version or find_version(source_dir)
477 version = normalize_windows_version(orig_version)
482 version = normalize_windows_version(orig_version)
478 print('using version string: %s' % version)
483 print('using version string: %s' % version)
479 if version != orig_version:
484 if version != orig_version:
480 print('(normalized from: %s)' % orig_version)
485 print('(normalized from: %s)' % orig_version)
481
486
482 if signing_info:
487 if signing_info:
483 sign_with_signtool(
488 sign_with_signtool(
484 staging_dir / "hg.exe",
489 staging_dir / "hg.exe",
485 "%s %s" % (signing_info["name"], version),
490 "%s %s" % (signing_info["name"], version),
486 subject_name=signing_info["subject_name"],
491 subject_name=signing_info["subject_name"],
487 cert_path=signing_info["cert_path"],
492 cert_path=signing_info["cert_path"],
488 cert_password=signing_info["cert_password"],
493 cert_password=signing_info["cert_password"],
489 timestamp_url=signing_info["timestamp_url"],
494 timestamp_url=signing_info["timestamp_url"],
490 )
495 )
491
496
492 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
497 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
493
498
494 wix_pkg, wix_entry = download_entry('wix', build_dir)
499 wix_pkg, wix_entry = download_entry('wix', build_dir)
495 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
500 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
496
501
497 if not wix_path.exists():
502 if not wix_path.exists():
498 extract_zip_to_directory(wix_pkg, wix_path)
503 extract_zip_to_directory(wix_pkg, wix_path)
499
504
500 if python2:
505 if python2:
501 ensure_vc90_merge_modules(build_dir)
506 ensure_vc90_merge_modules(build_dir)
502
507
503 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
508 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
504
509
505 defines = {'Platform': arch}
510 defines = {'Platform': arch}
506
511
507 # Derive a .wxs file with the staged files.
512 # Derive a .wxs file with the staged files.
508 manifest_wxs = build_dir / 'stage.wxs'
513 manifest_wxs = build_dir / 'stage.wxs'
509 with manifest_wxs.open('w', encoding='utf-8') as fh:
514 with manifest_wxs.open('w', encoding='utf-8') as fh:
510 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
515 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
511
516
512 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
517 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
513
518
514 for source, rel_path in sorted((extra_wxs or {}).items()):
519 for source, rel_path in sorted((extra_wxs or {}).items()):
515 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
520 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
516
521
517 source = wix_dir / 'mercurial.wxs'
522 source = wix_dir / 'mercurial.wxs'
518 defines['Version'] = version
523 defines['Version'] = version
519 defines['Comments'] = 'Installs Mercurial version %s' % version
524 defines['Comments'] = 'Installs Mercurial version %s' % version
520
525
521 if python2:
526 if python2:
522 defines["PythonVersion"] = "2"
527 defines["PythonVersion"] = "2"
523 defines['VCRedistSrcDir'] = str(build_dir)
528 defines['VCRedistSrcDir'] = str(build_dir)
524 else:
529 else:
525 defines["PythonVersion"] = "3"
530 defines["PythonVersion"] = "3"
526
531
527 if (staging_dir / "lib").exists():
532 if (staging_dir / "lib").exists():
528 defines["MercurialHasLib"] = "1"
533 defines["MercurialHasLib"] = "1"
529
534
530 if extra_features:
535 if extra_features:
531 assert all(';' not in f for f in extra_features)
536 assert all(';' not in f for f in extra_features)
532 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
537 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
533
538
534 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
539 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
535
540
536 msi_path = (
541 msi_path = (
537 source_dir
542 source_dir
538 / 'dist'
543 / 'dist'
539 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
544 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
540 )
545 )
541
546
542 args = [
547 args = [
543 str(wix_path / 'light.exe'),
548 str(wix_path / 'light.exe'),
544 '-nologo',
549 '-nologo',
545 '-ext',
550 '-ext',
546 'WixUIExtension',
551 'WixUIExtension',
547 '-sw1076',
552 '-sw1076',
548 '-spdb',
553 '-spdb',
549 '-o',
554 '-o',
550 str(msi_path),
555 str(msi_path),
551 ]
556 ]
552
557
553 for source, rel_path in sorted((extra_wxs or {}).items()):
558 for source, rel_path in sorted((extra_wxs or {}).items()):
554 assert source.endswith('.wxs')
559 assert source.endswith('.wxs')
555 source = os.path.basename(source)
560 source = os.path.basename(source)
556 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
561 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
557
562
558 args.extend(
563 args.extend(
559 [
564 [
560 str(build_dir / 'stage.wixobj'),
565 str(build_dir / 'stage.wixobj'),
561 str(build_dir / 'mercurial.wixobj'),
566 str(build_dir / 'mercurial.wixobj'),
562 ]
567 ]
563 )
568 )
564
569
565 subprocess.run(args, cwd=str(source_dir), check=True)
570 subprocess.run(args, cwd=str(source_dir), check=True)
566
571
567 print('%s created' % msi_path)
572 print('%s created' % msi_path)
568
573
569 if signing_info:
574 if signing_info:
570 sign_with_signtool(
575 sign_with_signtool(
571 msi_path,
576 msi_path,
572 "%s %s" % (signing_info["name"], version),
577 "%s %s" % (signing_info["name"], version),
573 subject_name=signing_info["subject_name"],
578 subject_name=signing_info["subject_name"],
574 cert_path=signing_info["cert_path"],
579 cert_path=signing_info["cert_path"],
575 cert_password=signing_info["cert_password"],
580 cert_password=signing_info["cert_password"],
576 timestamp_url=signing_info["timestamp_url"],
581 timestamp_url=signing_info["timestamp_url"],
577 )
582 )
578
583
579 return {
584 return {
580 'msi_path': msi_path,
585 'msi_path': msi_path,
581 }
586 }
General Comments 0
You need to be logged in to leave comments. Login now