"""Build and package DSView host protocol 1 using an existing MSYS2 UCRT64 SDK.""" import argparse import hashlib import json import os from pathlib import Path import re import shutil import subprocess REVISION = '2e9e2c8e' def recipe_digest(): """Identify the patch, packaging recipe, adapters and their shared cores.""" recipe = Path(__file__).resolve().parent core = recipe.parents[1] / 'python/logic_analyzer/decoders' inputs = [recipe / 'build.py', recipe / 'setgui-host.patch', recipe / 'gate-pair-checkbox.patch', recipe / 'decoder-panel.patch', recipe / 'driver-summary.patch', recipe / 'windows-usb-events.patch', recipe / 'native/gate_summary.h'] inputs += sorted((recipe / 'decoders').rglob('*.py')) inputs += [core / (name + '.py') for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart')] digest = hashlib.sha256() for item in inputs: digest.update(item.relative_to(recipe.parents[1]).as_posix().encode('utf-8') + b'\0') digest.update(hashlib.sha256(item.read_bytes()).digest()) return digest.hexdigest() def runtime_is_current(output): output = Path(output) try: stamp = json.loads((output / 'host-build.json').read_text(encoding='utf-8')) return (stamp.get('recipe') == recipe_digest() and stamp.get('executable') == hashlib.sha256((output / 'DSView.exe').read_bytes()).hexdigest() and (output / 'host-protocol-1').read_text(encoding='utf-8').strip() == 'DSView host protocol 1') except (OSError, ValueError, AttributeError): return False def package_decoders(output): """Stage sigrok adapters and the canonical parsing cores without forks.""" recipe = Path(__file__).resolve().parent decoders = Path(output) / 'decoders' shutil.copytree(recipe / 'decoders', decoders, dirs_exist_ok=True, ignore=shutil.ignore_patterns('__pycache__')) common = decoders / 'common/setgui_decoders' common.mkdir(parents=True, exist_ok=True) (common / '__init__.py').write_text('', encoding='utf-8') core = recipe.parents[1] / 'python/logic_analyzer/decoders' for name in ('gate_timing', 'transistor_pair', 'set_uart', 'set_can', 'pm35_uart'): shutil.copy2(core / (name + '.py'), common / (name + '.py')) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--source', type=Path, required=True) parser.add_argument('--output', type=Path, required=True) parser.add_argument('--msys-root', type=Path, default=Path(r'C:\setcorp\tools\msys64')) args = parser.parse_args() source, output = args.source.resolve(), args.output.resolve() prefix = args.msys_root.resolve() / 'ucrt64' if not (prefix / 'bin/cmake.exe').is_file(): raise SystemExit('Install the MSYS2 UCRT64 dependencies listed in README.md first.') revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=source, text=True).strip() if not revision.startswith(REVISION): raise SystemExit('Expected DSView revision ' + REVISION + ', got ' + revision) for retired_name in ('driver-summary.patch', 'gate-pair-checkbox.patch'): retired = Path(__file__).with_name(retired_name) if subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(retired)], cwd=source, capture_output=True).returncode == 0: subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', str(retired)], cwd=source, check=True) for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'): patch = Path(__file__).with_name(patch_name) applied = subprocess.run(['git', 'apply', '--ignore-space-change', '--reverse', '--check', str(patch)], cwd=source, capture_output=True) if applied.returncode: subprocess.run(['git', 'apply', '--ignore-space-change', '--check', str(patch)], cwd=source, check=True) subprocess.run(['git', 'apply', '--ignore-space-change', str(patch)], cwd=source, check=True) env = dict(os.environ) env['PATH'] = os.pathsep.join([str(prefix/'bin'), str(args.msys_root/'usr/bin'), env.get('PATH', '')]) env['PKG_CONFIG_PATH'] = str(prefix/'lib/pkgconfig') build = source/'build-win64' subprocess.run([str(prefix/'bin/cmake.exe'), '-S', str(source), '-B', str(build), '-G', 'Ninja', '-DCMAKE_POLICY_VERSION_MINIMUM=3.5', '-DCMAKE_CXX_STANDARD=11', '-DCMAKE_POLICY_DEFAULT_CMP0167=OLD', '-DCMAKE_PREFIX_PATH='+prefix.as_posix()], env=env, check=True) subprocess.run([str(prefix/'bin/cmake.exe'), '--build', str(build), '--parallel', '6'], env=env, check=True) output.mkdir(parents=True, exist_ok=True) shutil.copy2(source/'build.dir/DSView.exe', output/'DSView.exe') for relative in ('DSView/res', 'DSView/demo', 'libsigrokdecode4DSL/decoders', 'lang'): item = source/relative shutil.copytree(item, output/item.name, dirs_exist_ok=True, ignore=shutil.ignore_patterns('__pycache__')) package_decoders(output) for name in ('COPYING', 'NEWS25', 'NEWS31', 'ug25.pdf', 'ug31.pdf'): shutil.copy2(source/name, output/name) stdlib = sorted((prefix/'lib').glob('python3.*')) stdlib = [item for item in stdlib if (item/'encodings').is_dir()] if len(stdlib) != 1: raise RuntimeError('Expected exactly one Python standard library in UCRT64') shutil.copytree(stdlib[0], output/'lib'/stdlib[0].name, dirs_exist_ok=True, ignore=shutil.ignore_patterns('__pycache__', 'site-packages', 'test', 'tests', 'idlelib', 'tkinter', 'ensurepip')) for name in ('platforms', 'imageformats', 'iconengines', 'styles'): shutil.copytree(prefix/'share/qt5/plugins'/name, output/name, dirs_exist_ok=True) shutil.copytree(prefix/'share/licenses', output/'licenses', dirs_exist_ok=True) (output/'qt.conf').write_text('[Paths]\nPrefix=.\nPlugins=.\n', encoding='utf-8') queue = [p for p in output.rglob('*') if p.suffix.lower() in ('.exe', '.dll', '.pyd')] seen = set() while queue: binary = queue.pop() if binary in seen: continue seen.add(binary) info = subprocess.check_output([str(prefix/'bin/objdump.exe'), '-p', str(binary)], env=env, text=True, errors='replace') for dll in re.findall(r'DLL Name:\s*(\S+)', info): dependency, target = prefix/'bin'/dll, output/dll if dependency.is_file(): if target not in seen: shutil.copy2(dependency, target) queue.append(target) elif not (Path(os.environ['SystemRoot'])/'System32'/dll).is_file() and not dll.lower().startswith(('api-ms-', 'ext-ms-')): raise RuntimeError('Missing dependency: ' + dll) check_env = dict(os.environ) check_env['PATH'] = str(Path(os.environ['SystemRoot'])/'System32') version = subprocess.check_output([str(output/'DSView.exe'), '--host-version'], env=check_env, text=True).strip() if version != 'DSView host protocol 1': raise RuntimeError('Host protocol self-check failed: ' + version) (output/'host-protocol-1').write_text(version+'\n', encoding='utf-8') for patch_name in ('setgui-host.patch', 'decoder-panel.patch', 'windows-usb-events.patch'): shutil.copy2(Path(__file__).with_name(patch_name), output/patch_name) (output/'SOURCE.txt').write_text('https://github.com/DreamSourceLab/DSView\nCommit: '+revision+'\nPatches: setgui-host.patch, decoder-panel.patch, windows-usb-events.patch\nLicense: GPL-3.0-or-later (COPYING)\n', encoding='utf-8') (output/'host-build.json').write_text(json.dumps({ 'recipe': recipe_digest(), 'executable': hashlib.sha256((output/'DSView.exe').read_bytes()).hexdigest(), }, indent=2) + '\n', encoding='utf-8') print('DSView runtime:', output) if __name__ == '__main__': main()