packaging: stage installed files for Inno
authorGregory Szorc <gregory.szorc@gmail.com>
Wed, 23 Oct 2019 18:39:28 -0700
changeset 43516 d053d3f10b6a
parent 43515 7bd88d0d6a82
child 43517 24633444ff32
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
contrib/packaging/hgpackaging/inno.py
contrib/packaging/hgpackaging/py2exe.py
contrib/packaging/hgpackaging/util.py
contrib/packaging/inno/mercurial.iss
--- a/contrib/packaging/hgpackaging/inno.py	Wed Oct 23 18:39:17 2019 -0700
+++ b/contrib/packaging/hgpackaging/inno.py	Wed Oct 23 18:39:28 2019 -0700
@@ -14,7 +14,10 @@
 
 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 @@
     '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 @@
     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 @@
         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 @@
         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 @@
             % (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:
--- a/contrib/packaging/hgpackaging/py2exe.py	Wed Oct 23 18:39:17 2019 -0700
+++ b/contrib/packaging/hgpackaging/py2exe.py	Wed Oct 23 18:39:28 2019 -0700
@@ -15,10 +15,43 @@
 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 @@
         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)
--- a/contrib/packaging/hgpackaging/util.py	Wed Oct 23 18:39:17 2019 -0700
+++ b/contrib/packaging/hgpackaging/util.py	Wed Oct 23 18:39:28 2019 -0700
@@ -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 @@
         '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))
--- a/contrib/packaging/inno/mercurial.iss	Wed Oct 23 18:39:17 2019 -0700
+++ b/contrib/packaging/inno/mercurial.iss	Wed Oct 23 18:39:28 2019 -0700
@@ -33,8 +33,8 @@
 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 @@
 {{ '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/