Mercurial > public > mercurial-scm > hg-stable
view contrib/packaging/hgpackaging/util.py @ 53013:d333e14477f1
packaging: adapt `__version__.py` parsing for `setuptools_scm` output
The file is now generated by `setuptools_scm`, which stores values as strings
with strong quotes. This avoids the following failure at the end of the Inno
installer build:
<snip>
creating installer
Traceback (most recent call last):
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\packaging.py", line 70, in <module>
run()
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\packaging.py", line 62, in run
cli.main()
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\hgpackaging\cli.py", line 154, in main
args.func(**kwargs)
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\hgpackaging\cli.py", line 35, in build_inno
inno.build_with_pyoxidizer(
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\hgpackaging\inno.py", line 55, in build_with_pyoxidizer
build_installer(
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\hgpackaging\inno.py", line 146, in build_installer
version = read_version_py(source_dir)
File "c:\Users\Matt\projects\mercurial\mercurial-devel\contrib\packaging\hgpackaging\util.py", line 188, in read_version_py
raise Exception('could not parse %s' % p)
Exception: could not parse c:\Users\Matt\projects\mercurial\mercurial-devel\mercurial\__version__.py
Note that non-tagged builds end up with complex version strings, and include
characters that WiX rejects. That's probably fine- we don't do nightly or other
non-tagged builds for installers.
Also note that while the Inno installer is capable of using this version string
as part of the installer, the WiX installer is not for some reason. That
installer needs to be built with `--version VERSION` to inject the version into
the installer metadata and the filename.
author | Matt Harbison <matt_harbison@yahoo.com> |
---|---|
date | Mon, 24 Feb 2025 12:14:15 -0500 |
parents | 17d5e25b8e78 |
children |
line wrap: on
line source
# util.py - Common packaging utility code. # # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com> # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. # no-check-code because Python 3 native. import glob import os import pathlib import re import shutil import subprocess import zipfile def extract_zip_to_directory(source: pathlib.Path, dest: pathlib.Path): with zipfile.ZipFile(source, 'r') as zf: zf.extractall(dest) def find_vc_runtime_dll(x64=False): """Finds Visual C++ Runtime DLL to include in distribution.""" # We invoke vswhere to find the latest Visual Studio install. vswhere = ( pathlib.Path(os.environ["ProgramFiles(x86)"]) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe" ) if not vswhere.exists(): raise Exception( "could not find vswhere.exe: %s does not exist" % vswhere ) args = [ str(vswhere), # -products * is necessary to return results from Build Tools # (as opposed to full IDE installs). "-products", "*", "-requires", "Microsoft.VisualCpp.Redist.14.Latest", "-latest", "-property", "installationPath", ] vs_install_path = pathlib.Path( os.fsdecode(subprocess.check_output(args).strip()) ) # This just gets us a path like # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community # Actually vcruntime140.dll is under a path like: # VC\Redist\MSVC\<version>\<arch>\Microsoft.VC14<X>.CRT\vcruntime140.dll. arch = "x64" if x64 else "x86" search_glob = ( r"%s\VC\Redist\MSVC\*\%s\Microsoft.VC14*.CRT\vcruntime140.dll" % (vs_install_path, arch) ) candidates = glob.glob(search_glob, recursive=True) for candidate in reversed(candidates): return pathlib.Path(candidate) raise Exception("could not find vcruntime140.dll") def normalize_windows_version(version): """Normalize Mercurial version string so WiX/Inno accepts it. Version strings have to be numeric ``A.B.C[.D]`` to conform with MSI's requirements. We normalize RC version or the commit count to a 4th version component. We store this in the 4th component because ``A.B.C`` releases do occur and we want an e.g. ``5.3rc0`` version to be semantically less than a ``5.3.1rc2`` version. This requires always reserving the 3rd version component for the point release and the ``X.YrcN`` release is always point release 0. In the case of an RC and presence of ``+`` suffix data, we can't use both because the version format is limited to 4 components. We choose to use RC and throw away the commit count in the suffix. This means we could produce multiple installers with the same normalized version string. >>> normalize_windows_version("5.3") '5.3.0' >>> normalize_windows_version("5.3rc0") '5.3.0.0' >>> normalize_windows_version("5.3rc1") '5.3.0.1' >>> normalize_windows_version("5.3rc1+hg2.abcdef") '5.3.0.1' >>> normalize_windows_version("5.3+hg2.abcdef") '5.3.0.2' """ if '+' in version: version, extra = version.split('+', 1) else: extra = None # 4.9rc0 if version[:-1].endswith('rc'): rc = int(version[-1:]) version = version[:-3] else: rc = None # Ensure we have at least X.Y version components. versions = [int(v) for v in version.split('.')] while len(versions) < 3: versions.append(0) if len(versions) < 4: if rc is not None: versions.append(rc) elif extra: # hg<commit count>.<hash>+<date> versions.append(int(extra.split('.')[0][2:])) return '.'.join('%d' % x for x in versions[0:4]) 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)) def read_version_py(source_dir): """Read the mercurial/__version__.py file to resolve the version string.""" p = source_dir / 'mercurial' / '__version__.py' with p.open('r', encoding='utf-8') as fh: m = re.search("version = '([^']+)'", fh.read(), re.MULTILINE) if not m: raise Exception('could not parse %s' % p) return m.group(1)