# HG changeset patch # User Gregory Szorc # Date 2019-10-24 01:39:28 # Node ID d053d3f10b6a36c75de23aeccf3d63699b9e6f74 # Parent 7bd88d0d6a82b459e3b8a8a6acbc72167db444d6 packaging: stage installed files for Inno Previously, the Inno installer maintained its own mapping of source files to install location. (We have to maintain a similar mapping in the WiX installer.) Managing the explicit file layout for Windows packages is cumbersome and redundant. Every time you want to change the layout you need to change N locations. We frequently forget to do this and we only find out when people install Mercurial from our packages at release time. This commit starts the process of consolidating and simplifying the logic for managing the install layout on Windows. We introduce a list of install layout rules. These are simply source filenames (which can contain wildcards) and destination paths. The Inno packaging code has been updated to assemble all files into a staging directory that mirrors the final install layout. The list of files to add to the installer is derived by walking this staging directory and dynamically emitting the proper entries for the Inno Setup script. I diffed the file layout before and after this commit and there is no difference. Another benefit of this change is that it facilitates easier testing of the Windows install layout. Before, in order to test the final install layout, you needed to build an installer and run it. Now, you can stage files into the final layout and test from there, without running the installer. This should cut down on overhead when changing Windows code. Differential Revision: https://phab.mercurial-scm.org/D7159 diff --git a/contrib/packaging/hgpackaging/inno.py b/contrib/packaging/hgpackaging/inno.py --- a/contrib/packaging/hgpackaging/inno.py +++ b/contrib/packaging/hgpackaging/inno.py @@ -14,7 +14,10 @@ import subprocess import jinja2 -from .py2exe import build_py2exe +from .py2exe import ( + build_py2exe, + stage_install, +) from .util import find_vc_runtime_files EXTRA_PACKAGES = { @@ -24,6 +27,11 @@ EXTRA_PACKAGES = { 'win32ctypes', } +PACKAGE_FILES_METADATA = { + 'ReadMe.html': 'Flags: isreadme', + 'hg.exe': "AfterInstall: Touch('{app}\\hg.exe.local')", +} + def build( source_dir: pathlib.Path, @@ -47,6 +55,7 @@ def build( arch = 'x64' if vc_x64 else 'x86' inno_source_dir = source_dir / 'contrib' / 'packaging' / 'inno' inno_build_dir = build_dir / ('inno-%s' % arch) + staging_dir = inno_build_dir / 'stage' requirements_txt = ( source_dir / 'contrib' / 'packaging' / 'inno' / 'requirements.txt' @@ -63,6 +72,15 @@ def build( extra_packages=EXTRA_PACKAGES, ) + # Purge the staging directory for every build so packaging is + # pristine. + if staging_dir.exists(): + print('purging %s' % staging_dir) + shutil.rmtree(staging_dir) + + # Now assemble all the packaged files into the staging directory. + stage_install(source_dir, staging_dir) + # hg.exe depends on VC9 runtime DLLs. Copy those into place. for f in find_vc_runtime_files(vc_x64): if f.name.endswith('.manifest'): @@ -70,11 +88,34 @@ def build( else: basename = f.name - dest_path = source_dir / 'dist' / basename + dest_path = staging_dir / basename print('copying %s to %s' % (f, dest_path)) shutil.copyfile(f, dest_path) + # The final package layout is simply a mirror of the staging directory. + package_files = [] + for root, dirs, files in os.walk(staging_dir): + dirs.sort() + + root = pathlib.Path(root) + + for f in sorted(files): + full = root / f + rel = full.relative_to(staging_dir) + if str(rel.parent) == '.': + dest_dir = '{app}' + else: + dest_dir = '{app}\\%s' % rel.parent + + package_files.append( + { + 'source': rel, + 'dest_dir': dest_dir, + 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None), + } + ) + print('creating installer') # Install Inno files by rendering a template. @@ -93,11 +134,17 @@ def build( % (e.name, e.lineno, e.message,) ) - content = template.render() + content = template.render(package_files=package_files) with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh: fh.write(content) + # Copy additional files used by Inno. + for p in ('mercurial.ico', 'postinstall.txt'): + shutil.copyfile( + source_dir / 'contrib' / 'win32' / p, inno_build_dir / p + ) + args = [str(iscc_exe)] if vc_x64: diff --git a/contrib/packaging/hgpackaging/py2exe.py b/contrib/packaging/hgpackaging/py2exe.py --- a/contrib/packaging/hgpackaging/py2exe.py +++ b/contrib/packaging/hgpackaging/py2exe.py @@ -15,10 +15,43 @@ from .downloads import download_entry from .util import ( extract_tar_to_directory, extract_zip_to_directory, + process_install_rules, python_exe_info, ) +STAGING_RULES = [ + ('contrib/bash_completion', 'Contrib/'), + ('contrib/hgk', 'Contrib/hgk.tcl'), + ('contrib/hgweb.fcgi', 'Contrib/'), + ('contrib/hgweb.wsgi', 'Contrib/'), + ('contrib/mercurial.el', 'Contrib/'), + ('contrib/mq.el', 'Contrib/'), + ('contrib/tcsh_completion', 'Contrib/'), + ('contrib/tcsh_completion_build.sh', 'Contrib/'), + ('contrib/vim/*', 'Contrib/Vim/'), + ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'), + ('contrib/win32/ReadMe.html', 'ReadMe.html'), + ('contrib/xml.rnc', 'Contrib/'), + ('contrib/zsh_completion', 'Contrib/'), + ('dist/hg.exe', './'), + ('dist/lib/*.dll', 'lib/'), + ('dist/lib/*.pyd', 'lib/'), + ('dist/lib/library.zip', 'lib/'), + ('dist/Microsoft.VC*.CRT.manifest', './'), + ('dist/msvc*.dll', './'), + ('dist/python*.dll', './'), + ('doc/*.html', 'Docs/'), + ('doc/style.css', 'Docs/'), + ('mercurial/help/**/*.txt', 'help/'), + ('mercurial/default.d/*.rc', 'default.d/'), + ('mercurial/locale/**/*', 'locale/'), + ('mercurial/templates/**/*', 'Templates/'), + ('CONTRIBUTORS', 'Contributors.txt'), + ('COPYING', 'Copying.txt'), +] + + def build_py2exe( source_dir: pathlib.Path, build_dir: pathlib.Path, @@ -169,3 +202,12 @@ def build_py2exe( env=env, check=True, ) + + +def stage_install(source_dir: pathlib.Path, staging_dir: pathlib.Path): + """Copy all files to be installed to a directory. + + This allows packaging to simply walk a directory tree to find source + files. + """ + process_install_rules(STAGING_RULES, source_dir, staging_dir) diff --git a/contrib/packaging/hgpackaging/util.py b/contrib/packaging/hgpackaging/util.py --- a/contrib/packaging/hgpackaging/util.py +++ b/contrib/packaging/hgpackaging/util.py @@ -9,8 +9,10 @@ import distutils.version import getpass +import glob import os import pathlib +import shutil import subprocess import tarfile import zipfile @@ -164,3 +166,47 @@ def python_exe_info(python_exe: pathlib. 'version': version, 'py3': version >= distutils.version.LooseVersion('3'), } + + +def process_install_rules( + rules: list, source_dir: pathlib.Path, dest_dir: pathlib.Path +): + for source, dest in rules: + if '*' in source: + if not dest.endswith('/'): + raise ValueError('destination must end in / when globbing') + + # We strip off the source path component before the first glob + # character to construct the relative install path. + prefix_end_index = source[: source.index('*')].rindex('/') + relative_prefix = source_dir / source[0:prefix_end_index] + + for res in glob.glob(str(source_dir / source), recursive=True): + source_path = pathlib.Path(res) + + if source_path.is_dir(): + continue + + rel_path = source_path.relative_to(relative_prefix) + + dest_path = dest_dir / dest[:-1] / rel_path + + dest_path.parent.mkdir(parents=True, exist_ok=True) + print('copying %s to %s' % (source_path, dest_path)) + shutil.copy(source_path, dest_path) + + # Simple file case. + else: + source_path = pathlib.Path(source) + + if dest.endswith('/'): + dest_path = pathlib.Path(dest) / source_path.name + else: + dest_path = pathlib.Path(dest) + + full_source_path = source_dir / source_path + full_dest_path = dest_dir / dest_path + + full_dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(full_source_path, full_dest_path) + print('copying %s to %s' % (full_source_path, full_dest_path)) diff --git a/contrib/packaging/inno/mercurial.iss b/contrib/packaging/inno/mercurial.iss --- a/contrib/packaging/inno/mercurial.iss +++ b/contrib/packaging/inno/mercurial.iss @@ -33,8 +33,8 @@ ArchitecturesInstallIn64BitMode=x64 AppVerName=Mercurial {#VERSION} OutputBaseFilename=Mercurial-{#VERSION} #endif -InfoAfterFile=contrib/win32/postinstall.txt -LicenseFile=COPYING +InfoAfterFile=../postinstall.txt +LicenseFile=Copying.txt ShowLanguageDialog=yes AppPublisher=Matt Mackall and others AppPublisherURL=https://mercurial-scm.org/ @@ -43,49 +43,23 @@ AppUpdatesURL=https://mercurial-scm.org/ {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }} AppContact=mercurial@mercurial-scm.org DefaultDirName={pf}\Mercurial -SourceDir=..\.. +SourceDir=stage VersionInfoDescription=Mercurial distributed SCM (version {#VERSION}) VersionInfoCopyright=Copyright 2005-2019 Matt Mackall and others VersionInfoCompany=Matt Mackall and others InternalCompressLevel=max SolidCompression=true -SetupIconFile=contrib\win32\mercurial.ico +SetupIconFile=../mercurial.ico AllowNoIcons=true DefaultGroupName=Mercurial PrivilegesRequired=none ChangesEnvironment=true [Files] -Source: contrib\mercurial.el; DestDir: {app}/Contrib -Source: contrib\vim\*.*; DestDir: {app}/Contrib/Vim -Source: contrib\zsh_completion; DestDir: {app}/Contrib -Source: contrib\bash_completion; DestDir: {app}/Contrib -Source: contrib\tcsh_completion; DestDir: {app}/Contrib -Source: contrib\tcsh_completion_build.sh; DestDir: {app}/Contrib -Source: contrib\hgk; DestDir: {app}/Contrib; DestName: hgk.tcl -Source: contrib\xml.rnc; DestDir: {app}/Contrib -Source: contrib\mercurial.el; DestDir: {app}/Contrib -Source: contrib\mq.el; DestDir: {app}/Contrib -Source: contrib\hgweb.fcgi; DestDir: {app}/Contrib -Source: contrib\hgweb.wsgi; DestDir: {app}/Contrib -Source: contrib\win32\ReadMe.html; DestDir: {app}; Flags: isreadme -Source: contrib\win32\postinstall.txt; DestDir: {app}; DestName: ReleaseNotes.txt -Source: dist\hg.exe; DestDir: {app}; AfterInstall: Touch('{app}\hg.exe.local') -Source: dist\lib\*.dll; Destdir: {app}\lib -Source: dist\lib\*.pyd; Destdir: {app}\lib -Source: dist\python*.dll; Destdir: {app}; Flags: skipifsourcedoesntexist -Source: dist\msvc*.dll; DestDir: {app}; Flags: skipifsourcedoesntexist -Source: dist\Microsoft.VC*.CRT.manifest; DestDir: {app}; Flags: skipifsourcedoesntexist -Source: dist\lib\library.zip; DestDir: {app}\lib -Source: doc\*.html; DestDir: {app}\Docs -Source: doc\style.css; DestDir: {app}\Docs -Source: mercurial\help\*.txt; DestDir: {app}\help -Source: mercurial\help\internals\*.txt; DestDir: {app}\help\internals -Source: mercurial\default.d\*.rc; DestDir: {app}\default.d -Source: mercurial\locale\*.*; DestDir: {app}\locale; Flags: recursesubdirs createallsubdirs skipifsourcedoesntexist -Source: mercurial\templates\*.*; DestDir: {app}\Templates; Flags: recursesubdirs createallsubdirs -Source: CONTRIBUTORS; DestDir: {app}; DestName: Contributors.txt -Source: COPYING; DestDir: {app}; DestName: Copying.txt +{% for entry in package_files -%} +Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }} +{%- if entry.metadata %}; {{ entry.metadata }}{% endif %} +{% endfor %} [INI] Filename: {app}\Mercurial.url; Section: InternetShortcut; Key: URL; String: https://mercurial-scm.org/