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 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: 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 .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) 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 @@ '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 @@ 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/