Files
templates/tools/dsview/build.py

138 lines
7.8 KiB
Python

"""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()