diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71d27e37..51a35d89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -771,6 +771,94 @@ jobs: name: BeanNetworkTester-windows path: dist/BeanNetworkTester + # The Linux .tar.gz, built the way release.yml builds it: the same spec, the + # same pins, on the OLDEST Ubuntu the program supports (24.04, owner decision + # 2026-10-28), because the libraries in the bundle come from the system it is + # built on and start on that release and newer ones. A bundle built there was + # measured working on Ubuntu 26.04, Fedora 44 and Arch. Not in the branch + # ruleset, like the Linux capture job. + build-linux: + name: build the Linux package + runs-on: ubuntu-24.04 + timeout-minutes: 25 + needs: tests + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # The interpreter release.yml freezes - see the Windows build above. + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.15" + check-latest: true + cache: pip + # Tk's own library (setup-python's tkinter links the system's), and the + # display, window list and desktop-entry checker the smoke uses. + - name: Tk, Xvfb and the X tools + run: | + missing="" + for package in libtk8.6 xvfb x11-utils desktop-file-utils; do + dpkg -s "$package" >/dev/null 2>&1 || missing="$missing $package" + done + if [ -n "$missing" ]; then + echo "installing:$missing" + # The mirror and the two-step install: see "GUI render check". + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list \ + /etc/apt/sources.list.d/*.sources \ + /etc/apt/sources.list.d/*.list; do + if [ -f "$source" ]; then + sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' "$source" + fi + done + if ! sudo timeout 180 apt-get install -y --no-install-recommends $missing; then + sudo timeout 240 apt-get update \ + -o Acquire::Retries=2 \ + -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 + sudo timeout 240 apt-get install -y --no-install-recommends $missing + fi + fi + - name: Install dependencies + run: pip install --require-hashes -r requirements.txt -r requirements-build.txt + - name: Build + run: pyinstaller --noconfirm BeanNetworkTester.spec + # The font stack from the system, Tk, Xft and libebpfdivert in the bundle - + # tools/linux_bundle.py says what each costs when it is wrong. + - name: The libraries the bundle carries + run: | + python tools/linux_bundle.py check dist/BeanNetworkTester + find dist/BeanNetworkTester -name '*.so*' -printf '%f\n' | sort + - name: The package as a person runs it (CLI, repro line, user files, window) + run: | + timeout 300 xvfb-run -a --server-args="-screen 0 1280x800x24" \ + python -u tools/ci_linux_package_smoke.py dist/BeanNetworkTester/BeanNetworkTester + # A real capture by the frozen program, bounded as the Linux capture job's + # is: a namespace of its own, --dst-ip, --dst-port and --duration. + - name: A real capture by the package (namespace of its own) + run: | + sudo "$(command -v python)" tools/ci_linux_capture_smoke.py --require \ + --program dist/BeanNetworkTester/BeanNetworkTester + # The licence registry held to the Linux bundle, as the Windows build holds it + # to the zip (legal.LINUX_COMPONENTS; the reason is in that job). + - name: Scan the bundle and hold the licence registry to it + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + path: dist/BeanNetworkTester + format: syft-json + output-file: bundle-scan.json + upload-artifact: false + upload-release-assets: false + - name: The registry must account for everything the scan found + run: | + python -c "import json; print(sorted({a['name'] for a in json.load(open('bundle-scan.json'))['artifacts']}))" + python tools/sbom.py --audit-bundle bundle-scan.json + # A tar.gz, not the folder: an artifact zip loses the executable bit. + - name: Pack + run: tar -czf BeanNetworkTester-linux-x64.tar.gz -C dist BeanNetworkTester + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: BeanNetworkTester-linux + path: BeanNetworkTester-linux-x64.tar.gz + # 🔴 The weekly run tells nobody it went red. This job is the telling, and it # reverses the 2026-08-17 decision - see the note beside `schedule` at the top. # @@ -787,7 +875,7 @@ jobs: # case a second time, in tests/test_cron_issue.py. cron-issue: name: open an issue when the weekly run is red - needs: [public-text, lint, types, semgrep, audit, mutations, tests, linux-capture, build] + needs: [public-text, lint, types, semgrep, audit, mutations, tests, linux-capture, build, build-linux] # 🔴 `workflow_dispatch` is here so this path can be PROVEN. Until it was # added, the only way to find out whether `gh issue create` works with these # permissions was a red Monday - which is to say, the notice that exists diff --git a/BeanNetworkTester.spec b/BeanNetworkTester.spec index 266d4d7a..98c80fe4 100644 --- a/BeanNetworkTester.spec +++ b/BeanNetworkTester.spec @@ -34,42 +34,57 @@ # and "--license" can show them from any copy of the app. build.py additionally # places human-readable copies NEXT TO the exe: PyInstaller 6 puts datas inside # _internal/, and a licence nobody can find is a licence nobody reads. +# +# 6. One spec for both systems. +# The Linux .tar.gz is this same onedir bundle. On Linux there is no version +# resource or .ico (PyInstaller's win32 module does not even import there: +# "cannot import name 'win32api'", measured), and the bundle takes the font +# stack from the system instead of carrying it (tools/linux_bundle.py says why). import os import sys from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs -from PyInstaller.utils.win32.versioninfo import (FixedFileInfo, StringFileInfo, - StringStruct, StringTable, - VarFileInfo, VarStruct, - VSVersionInfo) + +WINDOWS = sys.platform == "win32" sys.path.insert(0, os.path.abspath(".")) from beantester.appinfo import (APP_NAME, AUTHOR, COPYRIGHT, EXE_NAME, VERSION_FILE, __version__) +# Off the search path again at once: the analysis below must not find tools/ there. +sys.path.insert(0, os.path.abspath("tools")) +import linux_bundle +sys.path.remove(os.path.abspath("tools")) + if __version__ == "0.0.0": raise SystemExit("BeanNetworkTester.spec: VERSION.txt is missing or malformed " "(expected x.y.z). Refusing to build an exe with no version.") _v = tuple(int(part) for part in __version__.split(".")[:3]) + (0,) -version_info = VSVersionInfo( - ffi=FixedFileInfo(filevers=_v, prodvers=_v, mask=0x3F, flags=0x0, OS=0x40004, - fileType=0x1, subtype=0x0), - kids=[ - StringFileInfo([StringTable("040904B0", [ - StringStruct("CompanyName", AUTHOR), - StringStruct("FileDescription", f"{APP_NAME} - poor network conditions simulator"), - StringStruct("FileVersion", __version__), - StringStruct("InternalName", "BeanNetworkTester"), - StringStruct("LegalCopyright", COPYRIGHT), - StringStruct("OriginalFilename", EXE_NAME), - StringStruct("ProductName", APP_NAME), - StringStruct("ProductVersion", __version__), - ])]), - VarFileInfo([VarStruct("Translation", [1033, 1200])]), - ], -) +version_info = None +if WINDOWS: + from PyInstaller.utils.win32.versioninfo import (FixedFileInfo, StringFileInfo, + StringStruct, StringTable, + VarFileInfo, VarStruct, + VSVersionInfo) + version_info = VSVersionInfo( + ffi=FixedFileInfo(filevers=_v, prodvers=_v, mask=0x3F, flags=0x0, OS=0x40004, + fileType=0x1, subtype=0x0), + kids=[ + StringFileInfo([StringTable("040904B0", [ + StringStruct("CompanyName", AUTHOR), + StringStruct("FileDescription", f"{APP_NAME} - poor network conditions simulator"), + StringStruct("FileVersion", __version__), + StringStruct("InternalName", "BeanNetworkTester"), + StringStruct("LegalCopyright", COPYRIGHT), + StringStruct("OriginalFilename", EXE_NAME), + StringStruct("ProductName", APP_NAME), + StringStruct("ProductVersion", __version__), + ])]), + VarFileInfo([VarStruct("Translation", [1033, 1200])]), + ], + ) datas = [("lang", "lang"), (VERSION_FILE, "."), ("LICENSE", "."), ("THIRD-PARTY-NOTICES.md", "."), ("licenses", "licenses"), @@ -95,7 +110,8 @@ a = Analysis( # never touches `ssl` - and its only hashing is crashlog's sha1 fingerprint, which # `hashlib` computes via the built-in `_sha1` module when `_hashlib` is absent # (verified). Dropping them is ~7 MB off the release at no runtime cost. - excludes=["pytest", "ssl", "_ssl", "_hashlib"], + excludes=["pytest", "ssl", "_ssl", "_hashlib"] + + ([] if WINDOWS else list(linux_bundle.NOT_NEEDED_MODULES)), noarchive=False, ) @@ -123,6 +139,10 @@ _TCL_CRUFT = ("_tcl_data/tzdata/", "_tcl_data/msgs/", "_tk_data/msgs/") a.datas = [d for d in a.datas if not any(part in d[0].replace("\\", "/") for part in _TCL_CRUFT)] +if not WINDOWS: + a.binaries = [b for b in a.binaries if not (linux_bundle.taken_from_system(b[0]) + or linux_bundle.not_needed(b[0]))] + pyz = PYZ(a.pure) exe = EXE( @@ -131,7 +151,7 @@ exe = EXE( [], exclude_binaries=True, # onedir name="BeanNetworkTester", - icon="bean.ico" if os.path.exists("bean.ico") else None, + icon="bean.ico" if WINDOWS and os.path.exists("bean.ico") else None, console=True, # the CLI must have stdout/stderr and an exit code uac_admin=False, # asInvoker - the app elevates itself when needed version=version_info, # exe Properties -> Details (author, version) diff --git a/CHANGELOG.md b/CHANGELOG.md index adbd095f..3bccadf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol ssh, it opens without root at once. `BEAN_NO_ELEVATE=1` turns the question off. The files it saves stay yours, as under sudo. +- **Linux: a window the desktop can tell apart.** The window now has a class of its own, not + the "Tk" every Tk program shares, so the desktop does not group it with other Tk windows. + When the window cannot open because the system lacks a library it needs, the line names + that library instead of saying tkinter is missing, and the command line still works. + - **Ping on the Tools tab, to see the session's latency and loss arrive.** Type an address or a name and press **Start**: each ping is a row with its round trip and what came back, the last 60 are charted, and the totals say what was lost. The ping goes on while another tab is in diff --git a/README.md b/README.md index e22f2b90..9240a672 100644 --- a/README.md +++ b/README.md @@ -1092,6 +1092,8 @@ connections arriving from outside, are still blocked in silence. | `--simulate` | synthetic traffic instead of WinDivert (test with no Windows, no driver, no admin) | | `--gui` | open the GUI. Valid **on its own only** - the GUI has its own controls, so combining it with settings flags is a usage error (exit 2) rather than a silent headless run | | `--version` | print the version and exit | +| `--install-desktop` | the Linux package only: add the program to your desktop's applications menu, then exit. Run it **without** `sudo`, from the folder you keep the program in. After moving the folder, run it again | +| `--remove-desktop` | the Linux package only: remove that menu entry again, then exit | **Output and diagnostics** @@ -1566,6 +1568,7 @@ Every job in that workflow, and what a red one means: | `tests` | the suite, the GUI smoke, the real-Tk render check and the CLI assertions, on Linux and Windows | | `linux-capture` | the experimental Linux capture on a real kernel: the command line, as root, inside a network namespace of its own. The latency and the loss must reach the target and nothing else. Not required for a merge | | `build` | the Windows executable, smoke-tested, with the driver check and the licence registry scan | +| `build-linux` | the Linux package on Ubuntu 24.04: the libraries it carries, the program run as a person runs it (command line, window, menu entry) under Xvfb, and a real capture by the packaged program in a namespace of its own. Not required for a merge | | `cron-issue` | opens an issue when the weekly run goes red, deduplicated by which jobs failed. A cron is easy to miss and nothing else announces one. It also answers to a hand-fired run, which is the only way to exercise it without waiting for a red Monday. Never runs on a pull request, and never closes an issue by itself | @@ -1632,6 +1635,7 @@ beantester/ the implementation package clilog.py CLI output: log on stderr ([bean]), data on stdout (text/NDJSON) winenv.py Windows: admin, elevation (UAC), console detach, DPI linuxenv.py Linux: the window asks for root through polkit (pkexec) + desktop.py Linux package: its entry in the desktop's applications menu driver.py WinDivert driver lifecycle + --doctor / --cleanup-driver fields.py FIELD REGISTRY - single source of truth: type, label, unit, range, form section, profile scope, CLI flag diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index e31bc7af..fc207a1c 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -12,11 +12,17 @@ licences below. Where they conflict, the licence below wins for that component. To see the exact versions bundled in the copy you are holding, run: - BeanNetworkTester.exe --license + BeanNetworkTester.exe --license (Windows) + ./BeanNetworkTester --license (Linux) + +The Windows zip and the Linux `.tar.gz` carry different libraries. Each section says +which of the two carries it; one that says nothing is in both. The Linux package takes +some libraries from the system it runs on instead (fontconfig, freetype and the X +libraries libxcb needs); those are the system's own and are not shipped. --- -## WinDivert (`WinDivert64.dll`, `WinDivert64.sys`) +## WinDivert (`WinDivert64.dll`, `WinDivert64.sys`, the Windows zip) * Copyright (c) basil (basil@reqrypt.org) * Used under: **GNU Lesser General Public License, version 3 (LGPLv3)** @@ -48,6 +54,37 @@ machine is in test-signing mode). That is a Windows requirement, not ours. --- +## eBPFDivert (`libebpfdivert.so`, the Linux package) + +* Copyright (c) Fabio Falcinelli and eBPFDivert contributors +* Used under: **GNU Lesser General Public License, version 3 or later + (LGPL-3.0-or-later)** + (eBPFDivert is dual-licensed under GPL-2.0-or-later or LGPL-3.0-or-later; this + program uses it under the LGPL. It contains WinDivert's filter code, which carries + the same dual licence.) +* Licence text: `licenses/LGPL-3.0.txt`, `licenses/GPL-3.0.txt`, `licenses/GPL-2.0.txt` +* Source code: https://github.com/ffalcinelli/ebpfdivert +* Shipped as: one library, `_internal/pydivert/bpf/libebpfdivert.so`, which comes + with PyDivert's Linux wheel and is used unmodified. Its version is printed by + `./BeanNetworkTester --license`. eBPFDivert 0.0.5 carries libbpf 1.7.0, + elfutils 0.192 (libelf and libeu), Zstandard 1.5.7 and zlib 1.2.11 inside it: + its release script builds the first three from their own sources and links the + zlib of the manylinux_2_28 image it is built in (their sections are below). + +**Your LGPL rights, in practice.** The library is a separate file the program loads +from that folder at run time. You may build an interface-compatible +`libebpfdivert.so` from eBPFDivert's source - with a modified libbpf, libelf, +Zstandard or zlib inside, if you like - and replace the file. You may reverse +engineer Bean Network Tester to the extent necessary to debug such modifications. + +**Written offer:** for at least three years from the date of this release, the +Author will supply, on request and at no charge beyond the cost of delivery, the +complete corresponding source of the eBPFDivert version in this build, with the +libbpf and libelf it carries. The source is public at the address above. Contact: +https://donislawdev.com/ + +--- + ## PyDivert * Copyright (c) Fabio Falcinelli and PyDivert contributors @@ -106,7 +143,8 @@ version is linked above. Contact: https://donislawdev.com/ * Used under: **PSF License Agreement** * Licence text: `licenses/Python-LICENSE.txt` * Source code: https://www.python.org/downloads/source/ -* The Python interpreter and standard library are embedded in the executable. +* The Python interpreter and standard library are embedded in the executable. In + the Linux package the interpreter is `_internal/libpython3.*.so`. --- @@ -117,7 +155,8 @@ version is linked above. Contact: https://donislawdev.com/ * Used under: **Tcl/Tk licence (BSD-style)** * Licence text: `licenses/Tcl-Tk-LICENSE.txt` * Source code: https://www.tcl-lang.org/software/tcltk/ -* Provides the graphical user interface toolkit. +* Provides the graphical user interface toolkit. In the Linux package it is Tcl/Tk + 8.6 as Ubuntu 24.04 ships it (`libtcl8.6.so`, `libtk8.6.so`). --- @@ -145,6 +184,9 @@ version is linked above. Contact: https://donislawdev.com/ runtime. `--license` reports the version the build links against; note that it can read `1.3.1.zlib-ng`, because the `zlib` MODULE is built against zlib-ng while the DLL itself is genuine zlib. Both are under this licence. +* In the Linux package: `libz.so.1` from Ubuntu 24.04 (zlib 1.3), and zlib + 1.2.11 inside `libebpfdivert.so` (Copyright (C) 1995-2017 Jean-loup Gailly and + Mark Adler, as its own strings say). --- @@ -156,11 +198,67 @@ version is linked above. Contact: https://donislawdev.com/ incorporated software - libffi arrives with CPython and is licensed there * Source code: https://github.com/libffi/libffi * Ships as `libffi-8.dll`; it is what `ctypes` calls into, and this project - reaches the WinDivert driver and several Windows APIs through `ctypes`. + reaches the WinDivert driver and several Windows APIs through `ctypes`. In the + Linux package: `libffi.so.8` from Ubuntu 24.04 (libffi 3.4.6), through which + `ctypes` reaches eBPFDivert. + +--- + +## X.Org libraries (`libX11`, `libXext`, `libXft`, `libXrender`, `libXss`, the Linux package) + +* Copyright: the X Consortium, The Open Group, Keith Packard and many others; each + library's holders are listed in its own section of the licence file +* Used under: **MIT and X11-style permission notices** (permissive) +* Licence text: `licenses/X.Org-libraries.txt` - the copyright files Ubuntu 24.04 + ships with these libraries, unchanged +* Source code: https://gitlab.freedesktop.org/xorg/lib +* Ship as `_internal/libX11.so.6`, `libXext.so.6`, `libXft.so.2`, `libXrender.so.1` + and `libXss.so.1`: libX11 1.8.7, libXext 1.3.4, libXft 2.3.6, libXrender 0.9.10 and + libXss 1.2.3, as Ubuntu 24.04 builds them. Tk draws the window through them. + +--- + +## libbpf (inside `libebpfdivert.so`, the Linux package) + +* Copyright (c) the libbpf authors (Facebook, Isovalent and others) +* Used under: **GNU Lesser General Public License, version 2.1 (LGPL-2.1)** + (libbpf is dual-licensed under LGPL-2.1 or BSD-2-Clause) +* Licence text: `licenses/LGPL-2.1.txt`, `licenses/libbpf-LICENSE.txt` +* Source code: https://github.com/libbpf/libbpf, version 1.7.0 (eBPFDivert 0.0.5's + `third_party/libbpf` submodule points at the commit of its v1.7.0 tag) +* What eBPFDivert loads its kernel programs with. Replacing it means rebuilding + `libebpfdivert.so` - see eBPFDivert above. + +--- + +## elfutils (libelf, inside `libebpfdivert.so`, the Linux package) + +* Copyright (c) Red Hat, Inc. and the elfutils contributors +* Used under: **GNU Lesser General Public License, version 3 or later + (LGPL-3.0-or-later)** (libelf is dual-licensed under LGPL-3.0-or-later or + GPL-2.0-or-later) +* Licence text: `licenses/LGPL-3.0.txt`, `licenses/GPL-2.0.txt`, + `licenses/elfutils-LICENSE.txt` +* Source code: https://sourceware.org/elfutils/, version 0.192 (the release + tarball eBPFDivert 0.0.5 builds libelf and libeu, elfutils' helper library, from) +* What libbpf reads the kernel programs' object file with. Replacing it means + rebuilding `libebpfdivert.so` - see eBPFDivert above. + +--- + +## Zstandard (inside `libebpfdivert.so`, the Linux package) + +* Copyright (c) Meta Platforms, Inc. and affiliates +* Used under: **BSD 3-Clause License** (Zstandard is dual-licensed under + BSD-3-Clause or GPL-2.0) +* Licence text: `licenses/zstd-LICENSE.txt` +* Source code: https://github.com/facebook/zstd, version 1.5.7 (the release + tarball eBPFDivert 0.0.5 builds it from) +* libelf's decompression; nothing in this program calls it directly. --- -## libtommath +## libtommath (the Windows zip) * Copyright: none claimed - the authors dedicate the work to the public domain * Used under: **the Unlicense** (a public-domain dedication) @@ -173,7 +271,7 @@ version is linked above. Contact: https://donislawdev.com/ --- -## Microsoft C Runtime (`ucrtbase.dll`, `VCRUNTIME140*.dll`, `api-ms-win-*.dll`) +## Microsoft C Runtime (`ucrtbase.dll`, `VCRUNTIME140*.dll`, `api-ms-win-*.dll`, the Windows zip) * Copyright (c) Microsoft Corporation * Used under: **Microsoft's redistributable terms** for the Visual C++ runtime diff --git a/beantester/appinfo.py b/beantester/appinfo.py index 50943370..4436d386 100644 --- a/beantester/appinfo.py +++ b/beantester/appinfo.py @@ -20,6 +20,7 @@ TOOL_ID = "BeanNetworkTester" # machine-readable id used in reports LAUNCHER = "bean_network_tester.py" # entry script name (running from sources) EXE_NAME = "BeanNetworkTester.exe" # the shipped executable - CLI *and* GUI +LINUX_EXE_NAME = "BeanNetworkTester" # the same in the Linux .tar.gz VERSION_FILE = "VERSION.txt" # the only place a version number may live LICENSE_NAME = "GNU General Public License v3.0" # free & open source, copyleft (see LICENSE) @@ -60,15 +61,21 @@ def command_name(simulate=False): command runs ``--simulate``, which on Linux needs no root. """ from .paths import is_frozen - if is_frozen(): - return EXE_NAME if sys.platform.startswith("linux"): return linux_command(simulate) + if is_frozen(): + return EXE_NAME return f"python {LAUNCHER}" -def linux_command(simulate=False, executable=None, checkout=None, home=None): - """The script as Linux runs it from this checkout, the README's way: +def exe_name(): + """The shipped executable's file name on this system.""" + return LINUX_EXE_NAME if sys.platform.startswith("linux") else EXE_NAME + + +def linux_command(simulate=False, executable=None, checkout=None, home=None, frozen=None): + """The program as Linux runs it, the README's way: from the ``.tar.gz`` + ``sudo ~/BeanNetworkTester/BeanNetworkTester``, from a checkout ``sudo .venv/bin/python -B bean_network_tester.py``. MEASURED 2026-10-27 on Ubuntu 26.04: there is no ``python`` there, only @@ -79,12 +86,16 @@ def linux_command(simulate=False, executable=None, checkout=None, home=None): checkout) unless the command is a simulation, and the interpreter running now, named with no path from this machine - a report carrying the command is shared: relative to the checkout when it is inside it, from ``~`` when it is in the - person's home, else ``python3``. + person's home, else ``python3``. The frozen program the same way: from ``~``, + else ``./BeanNetworkTester``, which runs from its own folder. """ from . import paths executable = sys.executable if executable is None else executable - checkout = paths.PROJECT_ROOT if checkout is None else checkout home = _persons_home() if home is None else home + if paths.is_frozen() if frozen is None else frozen: + program = (home and _inside(executable, home, "~/")) or "./" + LINUX_EXE_NAME + return " ".join(([] if simulate else ["sudo"]) + [program]) + checkout = paths.PROJECT_ROOT if checkout is None else checkout interpreter = (_inside(executable, checkout, "") or (home and _inside(executable, home, "~/")) or "python3") return " ".join(([] if simulate else ["sudo"]) + [interpreter] @@ -124,4 +135,4 @@ def version_line(): def program_name(): """``prog`` for argparse (no ``python `` prefix - argparse adds no shell).""" from .paths import is_frozen - return EXE_NAME if is_frozen() else LAUNCHER + return exe_name() if is_frozen() else LAUNCHER diff --git a/beantester/cli.py b/beantester/cli.py index 1bce0add..d1703fe8 100644 --- a/beantester/cli.py +++ b/beantester/cli.py @@ -16,11 +16,12 @@ import argparse import json import math +import re import sys import threading import time -from . import appinfo, capture_backend, clilog, driver, exitcodes, linuxenv, winenv +from . import appinfo, capture_backend, clilog, desktop, driver, exitcodes, linuxenv, winenv from .appinfo import command_name, program_name, version_line from .clilog import CliLog from . import crashlog @@ -206,6 +207,12 @@ def build_arg_parser(): p.add_argument("--version", action="version", version=version_line()) p.add_argument("--license", action="store_true", help="print the licence and the third-party notices, then exit") + menu = p.add_mutually_exclusive_group() + menu.add_argument("--install-desktop", action="store_true", + help="the Linux package: add this program to your desktop's " + "applications menu (run it without sudo), then exit") + menu.add_argument("--remove-desktop", action="store_true", + help="the Linux package: remove that menu entry, then exit") p.add_argument("--gui", action="store_true", help="open the GUI (only valid on its own - the GUI has its " "own controls, so it takes no settings flags)") @@ -711,6 +718,27 @@ def _run_license(log): return exitcodes.OK +def _run_desktop(log, install): + """``--install-desktop`` / ``--remove-desktop``: the Linux package's menu entry + (``desktop.py``). Not here (not the package, root): 1; not written: 5.""" + from . import desktop + reason = desktop.why_not() + if reason: + _fail(exitcodes.RUNTIME, reason) + path, error = desktop.install() if install else desktop.remove() + if error: + _fail(exitcodes.IO, error) + if install: + log.info(f"Added to the applications menu: {path}") + log.info(f"It starts {sys.executable}. After moving this folder, run " + "--install-desktop again from its new place.") + elif path: + log.info(f"Removed the menu entry: {path}") + else: + log.info("There was no menu entry to remove.") + return exitcodes.OK + + def _run_doctor(log): ok, checks = driver.doctor() # Where the user's own files are is an environment fact like the rest of this @@ -1190,6 +1218,8 @@ def _run_command(args, log, sleep, clock, engine): "drop --gui to run these settings from the command line.") if args.license: return _run_license(log) + if args.install_desktop or args.remove_desktop: + return _run_desktop(log, args.install_desktop) if args.doctor: return _run_doctor(log) if args.cleanup_driver: @@ -1352,7 +1382,9 @@ def _run_gui(argv, relaunched=False): from .gui import App winenv.set_dpi_awareness() # before the Tk root exists - root = tk.Tk() + # The class the Linux menu entry names (StartupWMClass): Tk's default "Tk" + # matches no entry, and GNOME shows such a window as a program of its own. + root = tk.Tk(className=desktop.WINDOW_CLASS) App(root) except Exception as exc: code = _window_did_not_open(exc) @@ -1368,6 +1400,9 @@ def _run_gui(argv, relaunched=False): # The modules whose absence means "this Python has no Tk", not "this program broke". _TK_MODULES = ("tkinter", "_tkinter") +# The loader's words for a library it could not find (glibc's dlopen error). +_MISSING_LIBRARY = re.compile(r"(\S+\.so[.0-9]*): cannot open shared object file") + def _window_did_not_open(exc): """GUI mode ended before its window could be used: say why, where it is seen. @@ -1376,9 +1411,20 @@ def _window_did_not_open(exc): Tk that cannot start - used to be called that as well and recorded nowhere. And in the frozen build the line went to the null sink ``detach_console`` put in place of stderr, so a double-clicked exe just vanished with exit code 1. + + A Tk that is there but cannot load a library of the system is not "No + tkinter" either. MEASURED 2026-10-28: the Linux package on a system without + libXft said exactly that - the library is named instead (the system's package + for it has a different name on every distribution, so none is guessed). """ if isinstance(exc, ImportError) and exc.name in _TK_MODULES: - text = f"No tkinter. Use CLI mode, e.g.: {command_name(simulate=True)} --simulate --loss 5" + example = f"{command_name(simulate=True)} --simulate --loss 5" + missing = _MISSING_LIBRARY.search(str(exc)) + if missing: + text = (f"The window needs {missing.group(1)} from the system, and it is not " + f"installed. The command line works without it, e.g.: {example}") + else: + text = f"No tkinter. Use CLI mode, e.g.: {example}" else: text = f"The window could not open: {type(exc).__name__}: {exc}" if crashlog.record(exc, source="gui-start") is not None: @@ -1396,6 +1442,7 @@ def main(argv=None): # driver, which leave no Python traceback at all). A crash during start-up - the # kind a user cannot even describe - used to vanish completely. crashlog.install() + linuxenv.restore_library_path() # the frozen build's: not for the programs it starts argv = sys.argv[1:] if argv is None else argv # The copy winenv.elevate_self started says so with its last argument. It is # taken off before anything reads the arguments: left in, the GUI's copy would diff --git a/beantester/desktop.py b/beantester/desktop.py new file mode 100644 index 00000000..c76e74ba --- /dev/null +++ b/beantester/desktop.py @@ -0,0 +1,149 @@ +"""The Linux package's entry in the desktop's applications menu. + +``BeanNetworkTester --install-desktop`` writes one Desktop Entry file into the +person's ``$XDG_DATA_HOME/applications`` (else ``~/.local/share/applications``), +and ``--remove-desktop`` removes that file again (owner decision 2026-10-28: a +command-line option, not a script in the archive and not a button). + +What the entry is, and why: + +* ``Exec`` is the program itself, not pkexec: the window asks for root on its + own (``linuxenv``). The path is absolute, so a folder moved after this points + at nothing; running the option again from the new place repairs it. +* ``StartupWMClass`` is the window's class. Tk's default is "Tk" (measured with + xwininfo on the package), and GNOME shows a window whose class matches no entry + as a program of its own in the dock, without the entry's name and icon - so the + window's root is created with ``className=WINDOW_CLASS`` (``cli._run_gui``). +* Only the package (a frozen build on Linux): from sources ``Exec`` would be an + interpreter in a virtual environment, which is a different question. +* Never as root: under ``sudo`` the file would be root's, in the person's folder. + +Quoting follows the Desktop Entry Specification ("The Exec key"): the path goes in +double quotes with ``"``, `````, ``$`` and ``\\`` escaped by a backslash, ``%`` is +written ``%%``, and the value's own string escapes (``\\`` for a backslash) apply +on top. A path with a control character is refused rather than encoded, and so is +a program path with ``=``: "The name or path of the executable program may not +contain the equal sign", and no quoting carries one. +""" +import os +import posixpath +import sys + +from . import appinfo, crashlog, paths + +ENTRY_ID = "com.donislawdev.BeanNetworkTester" # reverse DNS, as the spec recommends +FILE_NAME = ENTRY_ID + ".desktop" +# Already in the form Tk gives a class: the first letter up, the rest down. MEASURED +# 2026-10-28 (xwininfo on the package): className "BeanNetworkTester" became the +# class "Beannetworktester", so an entry naming "BeanNetworkTester" matched nothing. +WINDOW_CLASS = "Beannetworktester" +ICON_NAME = "bean.png" +COMMENT = "Simulate a poor network: latency, loss, jitter, a narrow link" + +_EXEC_ESCAPED = '"`$\\' + + +def applications_dir(environ=None): + """``$XDG_DATA_HOME/applications`` (absolute only, as the XDG spec says), else + ``~/.local/share/applications``.""" + environ = os.environ if environ is None else environ + data_home = environ.get("XDG_DATA_HOME", "") + if not posixpath.isabs(data_home): + data_home = posixpath.join(environ.get("HOME") or os.path.expanduser("~"), + ".local", "share") + return posixpath.join(data_home, "applications") + + +def _refused(path): + return any(ord(c) < 32 or ord(c) == 127 for c in path) + + +def exec_value(program): + """``program`` as one quoted argument of an ``Exec`` key.""" + quoted = "".join("\\" + c if c in _EXEC_ESCAPED else c for c in program) + return ('"' + quoted + '"').replace("\\", "\\\\").replace("%", "%%") + + +def string_value(text): + """``text`` as a Desktop Entry string value (its backslash escape).""" + return text.replace("\\", "\\\\") + + +def entry_text(program, icon): + """The whole Desktop Entry file for ``program`` with ``icon``.""" + return "\n".join(( + "[Desktop Entry]", + "Type=Application", + "Version=1.5", + f"Name={appinfo.APP_NAME}", + f"Comment={COMMENT}", + f"Exec={exec_value(program)}", + f"Icon={string_value(icon)}", + "Terminal=false", + "Categories=Network;", + f"StartupWMClass={WINDOW_CLASS}", + "", + )) + + +def why_not(): + """Why this run cannot add or remove the entry, or None when it can.""" + if not sys.platform.startswith("linux"): + return "the menu entry is for the Linux package; this is not Linux" + if not paths.is_frozen(): + return ("the menu entry is for the Linux package (the .tar.gz); from sources, " + "start bean_network_tester.py as the README says") + if paths.running_as_root(): + return "run it without sudo: the entry belongs to you, and root's would stay root's" + return None + + +def install(program=None, icon=None, environ=None): + """Write the entry. Returns ``(path, None)``, or ``(None, why)`` when it was not + written. ``program`` and ``icon`` default to this package's.""" + reason = why_not() + if reason: + return None, reason + program = sys.executable if program is None else program + icon = paths.resource_path(ICON_NAME) if icon is None else icon + for path in (program, icon): + if _refused(path): + return None, f"the path {path!r} holds a control character; move the folder" + if "=" in program: + return None, (f"the path {program!r} holds '=', and a menu entry cannot start a " + "program there; move the folder") + folder = applications_dir(environ) + target = posixpath.join(folder, FILE_NAME) + temporary = None + try: + paths.make_dirs(folder) + # A name of its own (paths.temp_beside): two installs at once shared + # ".tmp", and one truncated what the other was writing. mkstemp + # makes it 0600 and os.replace keeps that - the person's own menu reads it, + # nobody else needs to, and no execute bit is set. + temporary = paths.temp_beside(target) + with open(temporary, "w", encoding="utf-8", newline="\n") as handle: + handle.write(entry_text(program, icon)) + os.replace(temporary, target) + except OSError as exc: + if temporary: + with crashlog.quiet("desktop.temporary"): # nothing half-written left behind + os.unlink(temporary) + return None, f"cannot write {target}: {exc.strerror or exc}" + return target, None + + +def remove(environ=None): + """Remove the entry. Returns ``(path, None)`` when one was removed, ``(None, + None)`` when there was none, ``(None, why)`` when it could not be.""" + reason = why_not() + if reason: + return None, reason + target = posixpath.join(applications_dir(environ), FILE_NAME) + try: + os.unlink(target) + except FileNotFoundError: + return None, None + except OSError as exc: + return None, f"cannot remove {target}: {exc.strerror or exc}" + return target, None diff --git a/beantester/legal.py b/beantester/legal.py index 0e49430d..957d2e5a 100644 --- a/beantester/legal.py +++ b/beantester/legal.py @@ -62,10 +62,12 @@ # with it - and the guard failing on that day is the point, not an inconvenience. CODESIGN_SHA256 = "47b79ad3cfa53ef846cad03a59148f8c981d0b1196891e48b8c8d7982b10c148" -# The components we ship, in the order a reader cares about. ``module`` is the -# import name used to report the real version at run time (None = not a Python -# package, so the version is fixed or reported by other means). -COMPONENTS = ( +# The components the Windows zip ships, in the order a reader cares about. +# ``module`` is the import name used to report the real version at run time (None = +# not a Python package, so the version is fixed or reported by other means). The +# Linux .tar.gz ships a different set (``LINUX_COMPONENTS`` below), and +# ``COMPONENTS`` is the one for the system this runs on. +WINDOWS_COMPONENTS = ( # (name, module, licence used, where the source lives, SPDX expression) # The prose licence is for a person reading `--license`; the SPDX expression is # for `tools/sbom.py`. Both, because neither serves the other's reader: a scanner @@ -127,6 +129,54 @@ "LicenseRef-Microsoft-Redistributable"), ) +_SHARED = ("PyDivert", "psutil", "Python", "Tcl/Tk", "PyInstaller (bootloader)", "zlib", + "libffi") + +# The Linux .tar.gz (2026-10-28), read off a bundle built on Ubuntu 24.04 - the +# libraries it carries beside the program come from that system's packages (the +# X.Org ones' copyright files are licenses/X.Org-libraries.txt), and +# tools/linux_bundle.py says which ones the system provides instead. +# libebpfdivert.so, pydivert's Linux library, is a separate file that carries +# libbpf, elfutils' libelf, Zstandard and zlib inside it, built from their own +# sources rather than taken from a distribution (EBPFDIVERT_PARTS). +LINUX_COMPONENTS = ( + ("eBPFDivert", None, "LGPL-3.0-or-later (dual with GPL-2.0-or-later)", + "https://github.com/ffalcinelli/ebpfdivert", + "LGPL-3.0-or-later OR GPL-2.0-or-later"), + *(entry for entry in WINDOWS_COMPONENTS if entry[0] in _SHARED), + ("X.Org libraries", None, "MIT and X11-style permission notices", + "https://gitlab.freedesktop.org/xorg/lib", + "LicenseRef-X.Org-libraries"), + ("libbpf", None, "LGPL-2.1 (dual with BSD-2-Clause)", + "https://github.com/libbpf/libbpf", + "LGPL-2.1-only OR BSD-2-Clause"), + ("elfutils (libelf)", None, "LGPL-3.0-or-later (dual with GPL-2.0-or-later)", + "https://sourceware.org/elfutils/", + "LGPL-3.0-or-later OR GPL-2.0-or-later"), + ("Zstandard", None, "BSD-3-Clause (dual with GPL-2.0)", + "https://github.com/facebook/zstd", + "BSD-3-Clause OR GPL-2.0-only"), +) + +# The versions inside libebpfdivert.so, read for the eBPFDivert named here +# (2026-10-28): its release script, scripts/build_release.sh at that tag, builds +# Zstandard and elfutils from their release tarballs and libbpf from its +# third_party/libbpf submodule (the commit of libbpf's v1.7.0 tag), and links the +# zlib of the manylinux_2_28 image; the library's own strings say zstd 1.5.7 and +# zlib 1.2.11. A pydivert that brings another eBPFDivert reddens +# tests/test_license_surface.py on Linux until these are read again. +EBPFDIVERT_READ = "0.0.5" +EBPFDIVERT_PARTS = {"libbpf": "1.7.0", "elfutils (libelf)": "0.192", "Zstandard": "1.5.7"} + + +def components(platform=None): + """The components the build for ``platform`` (default: this one) ships.""" + platform = sys.platform if platform is None else platform + return LINUX_COMPONENTS if platform.startswith("linux") else WINDOWS_COMPONENTS + + +COMPONENTS = components() + def _module_version(name): """Version of an installed package, or ``-`` when it is not present here.""" @@ -191,10 +241,19 @@ def _pyinstaller_version(): return "bundled" -def component_rows(): +def _ebpfdivert_version(): + """The eBPFDivert pydivert loaded, as the library itself says, or ``-``.""" + try: + from pydivert import bpf + return bpf.libebpfdivert.ebpfdivert_version().decode() + except (ImportError, AttributeError, OSError, ValueError): # no pydivert, no library + return "-" + + +def component_rows(platform=None): """``(name, version, licence, source_url)`` for every third-party component.""" rows = [] - for name, module, licence, url, _spdx in COMPONENTS: + for name, module, licence, url, _spdx in components(platform): if module: version = _module_version(module) elif name == "Python": @@ -205,10 +264,15 @@ def component_rows(): version = _zlib_version() elif name == "WinDivert": version = WINDIVERT_VERSION + elif name == "eBPFDivert": + version = _ebpfdivert_version() elif name.startswith("PyInstaller"): version = _pyinstaller_version() + elif name in EBPFDIVERT_PARTS: + version = EBPFDIVERT_PARTS[name] else: - version = "bundled" # libffi and the MS runtime carry no version + version = "bundled" # libffi, the MS runtime, the X.Org libraries: + # no version to read rows.append((name, version, licence, url)) return rows diff --git a/beantester/linuxenv.py b/beantester/linuxenv.py index 68a0dd23..a4209640 100644 --- a/beantester/linuxenv.py +++ b/beantester/linuxenv.py @@ -24,6 +24,9 @@ process that asked (measured: that process stopped, the root window stayed). * Without ``--keep-cwd`` (polkit 121 and newer) it starts the program in root's home, where ``python -m beantester`` from a checkout cannot import itself. + +One knob is not about root: the frozen build's library path, which the programs +this one starts must not inherit (``restore_library_path``). """ import os import posixpath @@ -61,11 +64,38 @@ # The most of a parent's environment that is read. _ENVIRON_LIMIT = 1 << 20 +# Where PyInstaller's bootloader points the loader at the bundle's own libraries, +# and where it keeps the value the person had. +LIBRARY_PATH = "LD_LIBRARY_PATH" +LIBRARY_PATH_ORIG = "LD_LIBRARY_PATH_ORIG" + def is_linux(): return sys.platform.startswith("linux") +def restore_library_path(environ=None): + """Give the programs this one starts the library path the person had. + + The frozen build's bootloader points ``LD_LIBRARY_PATH`` at the bundle's own + libraries, and every program started from here inherits it. MEASURED + 2026-10-28 on Ubuntu 26.04, Fedora 44 and Arch: a system program then loads the + bundle's older fontconfig and stops ("fc-match: symbol lookup error"), and the + browser the window opens is such a program. This process keeps its libraries: + the loader read the variable when the process started. Only the frozen build + on Linux; True when it changed the environment. + """ + environ = os.environ if environ is None else environ + if not (is_linux() and winenv.is_frozen()): + return False + original = environ.pop(LIBRARY_PATH_ORIG, "") + if original: + environ[LIBRARY_PATH] = original + else: + environ.pop(LIBRARY_PATH, None) + return True + + def wants_root(environ=None, euid=None, which=shutil.which): """True when the window should ask for root before it opens. diff --git a/beantester/paths.py b/beantester/paths.py index 56ca04ee..55d41f3b 100644 --- a/beantester/paths.py +++ b/beantester/paths.py @@ -10,6 +10,7 @@ reason is package managers - see ``user_data_dir``. """ import os +import posixpath import shutil import stat import sys @@ -73,6 +74,10 @@ def user_data_dir(): ELEVATION - the GUI elevates itself, ``--simulate`` does not - and the same install would then keep two sets of profiles without saying so. + On Linux the frozen build (the ``.tar.gz``) writes into the person's + ``~/.config/BeanNetworkTester`` (``config_home``): unpacking a newer copy over + the old folder would take anything written beside the executable with it. + ``BEAN_DATA_DIR`` overrides everything. Running from sources is unchanged. """ override = os.environ.get(DATA_DIR_ENV, "").strip() @@ -84,9 +89,33 @@ def user_data_dir(): # resource_path (below), so a top-level import would run appinfo against a # half-initialised paths module. from .appinfo import TOOL_ID + if sys.platform.startswith("linux"): + return posixpath.join(config_home(), TOOL_ID) return os.path.join(_local_app_data(), TOOL_ID) +def config_home(environ=None, person=None, home=None): + """The folder Linux keeps a person's settings in: ``$XDG_CONFIG_HOME``, else + ``~/.config``. + + Root started for a person (``sudo``, ``pkexec``) uses THEIR folder: sudo's + environment carries root's ``XDG_CONFIG_HOME`` or none and pkexec's none, so it + is not read then, and the home comes from the account database. The XDG Base + Directory specification says a relative value is to be ignored. ``person`` + (``invoking_user``) and ``home`` (``home_of``) are injectable for the tests. + """ + environ = os.environ if environ is None else environ + person = invoking_user() if person is None else person + if person: + found = (home_of if home is None else home)(person[0]) + if found: + return posixpath.join(found, ".config") + value = environ.get("XDG_CONFIG_HOME", "") + if posixpath.isabs(value): + return value + return posixpath.join(environ.get("HOME") or os.path.expanduser("~"), ".config") + + def ensure_data_dir(): """Create the data directory. Returns an error message, or None. @@ -340,6 +369,8 @@ def migrate_user_files(source=None, target=None): explicit = source is not None or target is not None if not explicit and not is_frozen(): return [] # from sources the two are the same directory + if not explicit and sys.platform != "win32": + return [] # no build elsewhere ever kept files beside itself source = executable_dir() if source is None else source target = user_data_dir() if target is None else target if _same_directory(source, target): diff --git a/licenses/LGPL-2.1.txt b/licenses/LGPL-2.1.txt new file mode 100644 index 00000000..06319698 --- /dev/null +++ b/licenses/LGPL-2.1.txt @@ -0,0 +1,506 @@ +GNU Lesser General Public License, version 2.1, as Ubuntu 24.04 ships it +(/usr/share/common-licenses/LGPL-2.1). The licence of libbpf, which the Linux +package carries inside libebpfdivert.so. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/licenses/X.Org-libraries.txt b/licenses/X.Org-libraries.txt new file mode 100644 index 00000000..cc555ff0 --- /dev/null +++ b/licenses/X.Org-libraries.txt @@ -0,0 +1,1295 @@ +The X.Org client libraries in the Linux package of Bean Network Tester. +Each section is the copyright file Ubuntu 24.04 ships with the package the +library comes from, unchanged. + +============================================================================== +libX11 1.8.7 (Ubuntu 24.04 package libx11-6) +============================================================================== + +This package was downloaded from +https://xorg.freedesktop.org/releases/individual/lib/ + +The following is the 'standard copyright' agreed upon by most contributors, +and is currently the canonical license preferred by the X.Org Foundation. +This is a slight variant of the common MIT license form published by the +Open Source Initiative at https://opensource.org/licenses/mit-license.php + +Copyright holders of new code should use this license statement where +possible, and insert their name to this list. Please sort by surname +for people, and by the full name for other entities (e.g. Juliusz +Chroboczek sorts before Intel Corporation sorts before Daniel Stone). + +See each individual source file or directory for the license that applies +to that file. + +Copyright (C) 2003-2006,2008 Jamey Sharp, Josh Triplett +Copyright © 2009 Red Hat, Inc. +Copyright 1990-1992,1999,2000,2004,2009,2010 Oracle and/or its affiliates. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + ---------------------------------------------------------------------- + +The following licenses are 'legacy' - usually MIT/X11 licenses with the name +of the copyright holder(s) in the license statement: + +Copyright 1984-1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +X Window System is a trademark of The Open Group. + + ---------------------------------------- + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1994, 1996 X Consortium +Copyright 2000 The XFree86 Project, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the X Consortium shall +not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization +from the X Consortium. + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991 by +Digital Equipment Corporation + +Portions Copyright 1990, 1991 by Tektronix, Inc. + +Permission to use, copy, modify and distribute this documentation for +any purpose and without fee is hereby granted, provided that the above +copyright notice appears in all copies and that both that copyright notice +and this permission notice appear in all copies, and that the names of +Digital and Tektronix not be used in in advertising or publicity pertaining +to this documentation without specific, written prior permission. +Digital and Tektronix makes no representations about the suitability +of this documentation for any purpose. +It is provided ``as is'' without express or implied warranty. + + ---------------------------------------- + +Copyright (c) 1999-2000 Free Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FREE SOFTWARE FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the Free Software Foundation +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization from the +Free Software Foundation. + + ---------------------------------------- + +Code and supporting documentation (c) Copyright 1990 1991 Tektronix, Inc. + All Rights Reserved + +This file is a component of an X Window System-specific implementation +of Xcms based on the TekColor Color Management System. TekColor is a +trademark of Tektronix, Inc. The term "TekHVC" designates a particular +color space that is the subject of U.S. Patent No. 4,985,853 (equivalent +foreign patents pending). Permission is hereby granted to use, copy, +modify, sell, and otherwise distribute this software and its +documentation for any purpose and without fee, provided that: + +1. This copyright, permission, and disclaimer notice is reproduced in + all copies of this software and any modification thereof and in + supporting documentation; +2. Any color-handling application which displays TekHVC color + cooordinates identifies these as TekHVC color coordinates in any + interface that displays these coordinates and in any associated + documentation; +3. The term "TekHVC" is always used, and is only used, in association + with the mathematical derivations of the TekHVC Color Space, + including those provided in this file and any equivalent pathways and + mathematical derivations, regardless of digital (e.g., floating point + or integer) representation. + +Tektronix makes no representation about the suitability of this software +for any purpose. It is provided "as is" and with all faults. + +TEKTRONIX DISCLAIMS ALL WARRANTIES APPLICABLE TO THIS SOFTWARE, +INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. IN NO EVENT SHALL TEKTRONIX BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR THE PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +(c) Copyright 1995 FUJITSU LIMITED +This is source code modified by FUJITSU LIMITED under the Joint +Development Agreement for the CDE/Motif PST. + + ---------------------------------------- + +Copyright 1992 by Oki Technosystems Laboratory, Inc. +Copyright 1992 by Fuji Xerox Co., Ltd. + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Oki Technosystems +Laboratory and Fuji Xerox not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. +Oki Technosystems Laboratory and Fuji Xerox make no representations +about the suitability of this software for any purpose. It is provided +"as is" without express or implied warranty. + +OKI TECHNOSYSTEMS LABORATORY AND FUJI XEROX DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL OKI TECHNOSYSTEMS +LABORATORY AND FUJI XEROX BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991, 1992, 1993, 1994 by FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED +not be used in advertising or publicity pertaining to distribution +of the software without specific, written prior permission. +FUJITSU LIMITED makes no representations about the suitability of +this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJITSU LIMITED DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright (c) 1995 David E. Wexelblat. All rights reserved + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL DAVID E. WEXELBLAT BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of David E. Wexelblat shall +not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization +from David E. Wexelblat. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name OMRON not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. OMRON makes no representations +about the suitability of this software for any purpose. It is provided +"as is" without express or implied warranty. + +OMRON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL OMRON BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTUOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991 by +Digital Equipment Corporation + +Portions Copyright 1990, 1991 by Tektronix, Inc + +Rewritten for X.org by Chris Lee + +Permission to use, copy, modify, distribute, and sell this documentation +for any purpose and without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +Chris Lee makes no representations about the suitability for any purpose +of the information in this document. It is provided \`\`as-is'' without +express or implied warranty. + + ---------------------------------------- + +Copyright 1993 by Digital Equipment Corporation, Maynard, Massachusetts, +Copyright 1994 by FUJITSU LIMITED +Copyright 1994 by Sony Corporation + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the names of Digital, FUJITSU +LIMITED and Sony Corporation not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. + +DIGITAL, FUJITSU LIMITED AND SONY CORPORATION DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL DIGITAL, FUJITSU LIMITED +AND SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright 1991 by the Open Software Foundation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Open Software Foundation +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Open Software +Foundation makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OPEN SOFTWARE FOUNDATION DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL OPEN SOFTWARE FOUNDATIONN BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991, 1992,1993, 1994 by FUJITSU LIMITED +Copyright 1993, 1994 by Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of FUJITSU LIMITED and Sony Corporation +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. FUJITSU LIMITED and +Sony Corporation makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +FUJITSU LIMITED AND SONY CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL FUJITSU LIMITED OR SONY CORPORATION BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright (c) 1993, 1995 by Silicon Graphics Computer Systems, Inc. + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation, and that the name of Silicon Graphics not be +used in advertising or publicity pertaining to distribution +of the software without specific prior written permission. +Silicon Graphics makes no representation about the suitability +of this software for any purpose. It is provided "as is" +without any express or implied warranty. + +SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON +GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH +THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1991, 1992, 1993, 1994 by FUJITSU LIMITED +Copyright 1993 by Digital Equipment Corporation + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of FUJITSU LIMITED and +Digital Equipment Corporation not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. FUJITSU LIMITED and Digital Equipment Corporation +makes no representations about the suitability of this software for +any purpose. It is provided "as is" without express or implied +warranty. + +FUJITSU LIMITED AND DIGITAL EQUIPMENT CORPORATION DISCLAIM ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +FUJITSU LIMITED AND DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR +ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1992, 1993 by FUJITSU LIMITED +Copyright 1993 by Fujitsu Open Systems Solutions, Inc. +Copyright 1994 by Sony Corporation + +Permission to use, copy, modify, distribute and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED, +Fujitsu Open Systems Solutions, Inc. and Sony Corporation not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. +FUJITSU LIMITED, Fujitsu Open Systems Solutions, Inc. and +Sony Corporation make no representations about the suitability of +this software for any purpose. It is provided "as is" without +express or implied warranty. + +FUJITSU LIMITED, FUJITSU OPEN SYSTEMS SOLUTIONS, INC. AND SONY +CORPORATION DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, +IN NO EVENT SHALL FUJITSU OPEN SYSTEMS SOLUTIONS, INC., FUJITSU LIMITED +AND SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1987, 1988, 1990, 1993 by Digital Equipment Corporation, +Maynard, Massachusetts, + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + +Copyright 1993 by SunSoft, Inc. +Copyright 1999-2000 by Bruno Haible + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the names of SunSoft, Inc. and +Bruno Haible not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. SunSoft, Inc. and Bruno Haible make no representations +about the suitability of this software for any purpose. It is +provided "as is" without express or implied warranty. + +SunSoft Inc. AND Bruno Haible DISCLAIM ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL SunSoft, Inc. OR Bruno Haible BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the TOSHIBA Corp. + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of Open Software Foundation and TOSHIBA +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Open Software +Foundation and TOSHIBA make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OPEN SOFTWARE FOUNDATION AND TOSHIBA DISCLAIM ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL OPEN SOFTWARE FOUNDATIONN OR TOSHIBA BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1988 by Wyse Technology, Inc., San Jose, Ca., + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name Wyse not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +WYSE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + + +Copyright 1991 by the Open Software Foundation +Copyright 1993, 1994 by the Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of Open Software Foundation and +Sony Corporation not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +Open Software Foundation and Sony Corporation make no +representations about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +OPEN SOFTWARE FOUNDATION AND SONY CORPORATION DISCLAIM ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL OPEN +SOFTWARE FOUNDATIONN OR SONY CORPORATION BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1992, 1993 by FUJITSU LIMITED +Copyright 1993 by Fujitsu Open Systems Solutions, Inc. + +Permission to use, copy, modify, distribute and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED and +Fujitsu Open Systems Solutions, Inc. not be used in advertising or +publicity pertaining to distribution of the software without specific, +written prior permission. +FUJITSU LIMITED and Fujitsu Open Systems Solutions, Inc. makes no +representations about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJITSU LIMITED AND FUJITSU OPEN SYSTEMS SOLUTIONS, INC. DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL FUJITSU OPEN SYSTEMS +SOLUTIONS, INC. AND FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1993, 1994 by Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Sony Corporation +not be used in advertising or publicity pertaining to distribution +of the software without specific, written prior permission. +Sony Corporation makes no representations about the suitability of +this software for any purpose. It is provided "as is" without +express or implied warranty. + +SONY CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1986, 1998 The Open Group +Copyright (c) 2000 The XFree86 Project, Inc. + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +X CONSORTIUM OR THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name of the X Consortium or of the +XFree86 Project shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the X Consortium and the XFree86 Project. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, NTT, and +Open Software Foundation not be used in advertising or publicity +pertaining to distribution of the software without specific, +written prior permission. OMRON, NTT Software, NTT, and Open Software +Foundation make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OMRON, NTT SOFTWARE, NTT, AND OPEN SOFTWARE FOUNDATION +DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT +SHALL OMRON, NTT SOFTWARE, NTT, OR OPEN SOFTWARE FOUNDATION BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1988 by Wyse Technology, Inc., San Jose, Ca, +Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts, + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL AND WYSE DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL DIGITAL OR WYSE BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright 1991, 1992 by Fuji Xerox Co., Ltd. +Copyright 1992, 1993, 1994 by FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Fuji Xerox, +FUJITSU LIMITED not be used in advertising or publicity pertaining +to distribution of the software without specific, written prior +permission. Fuji Xerox, FUJITSU LIMITED make no representations +about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJI XEROX, FUJITSU LIMITED DISCLAIM ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL FUJI XEROX, +FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 2006 Josh Triplett + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + ---------------------------------------- + +(c) Copyright 1996 by Sebastien Marineau and Holger Veit + + + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +HOLGER VEIT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name of Sebastien Marineau or Holger Veit +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Holger Veit or +Sebastien Marineau. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the TOSHIBA Corp. +Copyright 1993, 1994 by Sony Corporation +Copyright 1993, 1994 by the FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, NTT, Open +Software Foundation, and Sony Corporation not be used in advertising +or publicity pertaining to distribution of the software without specific, +written prior permission. OMRON, NTT Software, NTT, Open Software +Foundation, and Sony Corporation make no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +OMRON, NTT SOFTWARE, NTT, OPEN SOFTWARE FOUNDATION, AND SONY +CORPORATION DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT +SHALL OMRON, NTT SOFTWARE, NTT, OPEN SOFTWARE FOUNDATION, OR SONY +CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 2000 by Bruno Haible + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Bruno Haible not +be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Bruno Haible +makes no representations about the suitability of this software for +any purpose. It is provided "as is" without express or implied +warranty. + +Bruno Haible DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT SHALL Bruno Haible BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright © 2003 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright (c) 2007-2009, Troy D. Hanson +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ---------------------------------------- + +Copyright 1992, 1993 by TOSHIBA Corp. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of TOSHIBA not be used in advertising +or publicity pertaining to distribution of the software without specific, +written prior permission. TOSHIBA make no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +TOSHIBA DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +TOSHIBA BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + + ---------------------------------------- + +Copyright IBM Corporation 1993 + +All Rights Reserved + +License to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of IBM not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +IBM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS, AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS, IN NO EVENT SHALL +IBM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, and NTT +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. OMRON, NTT Software, +and NTT make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OMRON, NTT SOFTWARE, AND NTT, DISCLAIM ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL OMRON, NTT SOFTWARE, OR NTT, BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +============================================================================== +libXext 1.3.4 (Ubuntu 24.04 package libxext6) +============================================================================== + +This package was downloaded from +http://xorg.freedesktop.org/releases/individual/lib/ + +Copyright 1986, 1987, 1988, 1989, 1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +Copyright (c) 1996 Digital Equipment Corporation, Maynard, Massachusetts. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY CLAIM, DAMAGES, INCLUDING, +BUT NOT LIMITED TO CONSEQUENTIAL OR INCIDENTAL DAMAGES, OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Digital Equipment Corporation +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Digital +Equipment Corporation. + +Copyright (c) 1997 by Silicon Graphics Computer Systems, Inc. +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation, and that the name of Silicon Graphics not be +used in advertising or publicity pertaining to distribution +of the software without specific prior written permission. +Silicon Graphics makes no representation about the suitability +of this software for any purpose. It is provided "as is" +without any express or implied warranty. +SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON +GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH +THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Copyright 1992 Network Computing Devices + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of NCD. not be used in advertising or +publicity pertaining to distribution of the software without specific, +written prior permission. NCD. makes no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +NCD. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL NCD. +BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Copyright 1991,1993 by Digital Equipment Corporation, Maynard, Massachusetts, +and Olivetti Research Limited, Cambridge, England. + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the names of Digital or Olivetti +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL AND OLIVETTI DISCLAIM ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL THEY BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Copyright 1986, 1987, 1988 by Hewlett-Packard Corporation + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation, and that the name of Hewlett-Packard not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +Hewlett-Packard makes no representations about the +suitability of this software for any purpose. It is provided +"as is" without express or implied warranty. + +This software is not subject to any license of the American +Telephone and Telegraph Company or of the Regents of the +University of California. + +Copyright (c) 1994, 1995 Hewlett-Packard Company + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL HEWLETT-PACKARD COMPANY BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the Hewlett-Packard +Company shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the Hewlett-Packard Company. + +Copyright Digital Equipment Corporation, 1996 + +Permission to use, copy, modify, distribute, and sell this +documentation for any purpose is hereby granted without fee, +provided that the above copyright notice and this permission +notice appear in all copies. Digital Equipment Corporation +makes no representations about the suitability for any purpose +of the information in this document. This documentation is +provided ``as is'' without express or implied warranty. + +Copyright (c) 1999, 2005, 2006, Oracle and/or its affiliates. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Copyright (c) 1989 X Consortium, Inc. and Digital Equipment Corporation. +Copyright (c) 1992 X Consortium, Inc. and Intergraph Corporation. +Copyright (c) 1993 X Consortium, Inc. and Silicon Graphics, Inc. +Copyright (c) 1994, 1995 X Consortium, Inc. and Hewlett-Packard Company. + +Permission to use, copy, modify, and distribute this documentation for +any purpose and without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +Digital Equipment Corporation, Intergraph Corporation, Silicon +Graphics, Hewlett-Packard, and the X Consortium make no +representations about the suitability for any purpose of the +information in this document. This documentation is provided ``as is'' +without express or implied warranty. + +============================================================================== +libXft 2.3.6 (Ubuntu 24.04 package libxft2) +============================================================================== + +This package was downloaded from +http://xorg.freedesktop.org/releases/individual/lib/ + +This package was originally Debianized by Colin Walters + on Sun, 13 Oct 2002 15:01:50 -0400. + +Later versions were Debianized by Branden Robinson . + +Upstream maintainer: Keith Packard + +Copyright and license: + +Copyright © 2001,2003 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +============================================================================== +libXrender 0.9.10 (Ubuntu 24.04 package libxrender1) +============================================================================== + +This package was downloaded from +https://xorg.freedesktop.org/releases/individual/lib/ + +Copyright © 2001,2003 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Copyright © 2000 SuSE, Inc. + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of SuSE not be used in advertising or +publicity pertaining to distribution of the software without specific, +written prior permission. SuSE makes no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +SuSE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL SuSE +BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +============================================================================== +libXss 1.2.3 (Ubuntu 24.04 package libxss1) +============================================================================== + +This package was downloaded from +https://xorg.freedesktop.org/releases/individual/lib/ + +Copyright (c) 1992 X Consortium + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the X Consortium shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from the X Consortium. + + +Copyright (C) 2003 The XFree86 Project, Inc. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. +IN NO EVENT SHALL THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the XFree86 Project +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +from the XFree86 Project. diff --git a/licenses/elfutils-LICENSE.txt b/licenses/elfutils-LICENSE.txt new file mode 100644 index 00000000..52b4c464 --- /dev/null +++ b/licenses/elfutils-LICENSE.txt @@ -0,0 +1,40 @@ +elfutils 0.192: libelf and libeu (elfutils' lib/), inside libebpfdivert.so in +the Linux package of Bean Network Tester. eBPFDivert 0.0.5 builds them from +the elfutils-0.192 release tarball. Every source file of both carries the +licence notice below; this one is the notice at the top of libelf/elf_begin.c, +unchanged. The licence texts are licenses/LGPL-3.0.txt and licenses/GPL-2.0.txt +(elfutils ships them as COPYING-LGPLV3 and COPYING-GPLV2). + +============================================================================== +libelf/elf_begin.c (elfutils 0.192) +============================================================================== + +/* Create descriptor for processing file. + Copyright (C) 1998-2010, 2012, 2014, 2015, 2016 Red Hat, Inc. + Copyright (C) 2021, 2022 Mark J. Wielaard + This file is part of elfutils. + Written by Ulrich Drepper , 1998. + + This file is free software; you can redistribute it and/or modify + it under the terms of either + + * the GNU Lesser General Public License as published by the Free + Software Foundation; either version 3 of the License, or (at + your option) any later version + + or + + * the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at + your option) any later version + + or both in parallel, as here. + + elfutils is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received copies of the GNU General Public License and + the GNU Lesser General Public License along with this program. If + not, see . */ diff --git a/licenses/libbpf-LICENSE.txt b/licenses/libbpf-LICENSE.txt new file mode 100644 index 00000000..2d1b0e5a --- /dev/null +++ b/licenses/libbpf-LICENSE.txt @@ -0,0 +1,47 @@ +libbpf 1.7.0, inside libebpfdivert.so in the Linux package of Bean Network +Tester. eBPFDivert 0.0.5 builds it from its third_party/libbpf submodule, +which points at the commit of libbpf's v1.7.0 tag. Each section is a file of +libbpf at that tag, unchanged. The LGPL 2.1 text is licenses/LGPL-2.1.txt. + +============================================================================== +LICENSE (libbpf v1.7.0) +============================================================================== + +LGPL-2.1 OR BSD-2-Clause + +============================================================================== +LICENSE.BSD-2-Clause (libbpf v1.7.0) +============================================================================== + +Valid-License-Identifier: BSD-2-Clause +SPDX-URL: https://spdx.org/licenses/BSD-2-Clause.html +Usage-Guide: + To use the BSD 2-clause "Simplified" License put the following SPDX + tag/value pair into a comment according to the placement guidelines in + the licensing rules documentation: + SPDX-License-Identifier: BSD-2-Clause +License-Text: + +Copyright (c) 2015 The Libbpf Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/zstd-LICENSE.txt b/licenses/zstd-LICENSE.txt new file mode 100644 index 00000000..d7d4810a --- /dev/null +++ b/licenses/zstd-LICENSE.txt @@ -0,0 +1,40 @@ +Zstandard 1.5.7, inside libebpfdivert.so in the Linux package of Bean Network +Tester. eBPFDivert 0.0.5 builds it from the zstd-1.5.7 release tarball. This is +Zstandard's LICENSE file at its v1.5.7 tag, unchanged: the BSD 3-Clause +License, under which this program uses it (Zstandard is dual-licensed under +it or the GPL 2.0). + +============================================================================== +LICENSE (Zstandard v1.5.7) +============================================================================== + +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/test_ci_linux_capture_smoke.py b/tests/test_ci_linux_capture_smoke.py index 81b44742..ecaefd17 100644 --- a/tests/test_ci_linux_capture_smoke.py +++ b/tests/test_ci_linux_capture_smoke.py @@ -281,3 +281,31 @@ def test_a_machine_that_cannot_run_it_skips_and_ci_turns_that_into_a_failure(cap check("--require: the same machine is a failure", smoke.main(["--require"], environment=("linux", 1000, True)) == 1) check("and says FAIL", "FAIL" in capsys.readouterr().out) + + +def test_the_sessions_run_the_program_they_are_given(monkeypatch): + """``--program``: the frozen Linux bundle runs the sessions, so CI captures for + real with the artefact a person downloads, not only with the source tree.""" + monkeypatch.setattr(smoke, "PROGRAM", None) + check("the source tree by default", + smoke.session_command(["--duration", "6"]) + == [sys.executable, "-m", "beantester", "--duration", "6"]) + monkeypatch.setattr(smoke, "PROGRAM", "/b/BeanNetworkTester") + check("the program given", smoke.session_command(["--duration", "6"]) + == ["/b/BeanNetworkTester", "--duration", "6"]) + + +def test_a_bundle_run_stops_after_the_sessions(monkeypatch): + """The tools' phases test the source tree's functions: with ``--program`` only + the two sessions run, and their failures still fail the run.""" + monkeypatch.setattr(smoke, "PROGRAM", None) # main sets it; undone after the test + ran = [] + for name in ("_latency_phase", "_loss_phase", "_ping_phase", "_reset_phase", + "_health_phase", "_adapters_phase"): + monkeypatch.setattr(smoke, name, lambda name=name: ran.append(name) or []) + for name in ("build_namespace", "teardown_namespace", "start_far_end"): + monkeypatch.setattr(smoke, name, lambda: None) + monkeypatch.setattr(smoke, "_loss_phase", lambda: ran.append("_loss_phase") or ["lost"]) + code = smoke.main(["--program", "/b/BeanNetworkTester"], environment=("linux", 0, True)) + check("only the sessions", ran == ["_latency_phase", "_loss_phase"], f"({ran})") + check("and a failure among them fails the run", code == 1) diff --git a/tests/test_ci_linux_package_smoke.py b/tests/test_ci_linux_package_smoke.py new file mode 100644 index 00000000..27238d47 --- /dev/null +++ b/tests/test_ci_linux_package_smoke.py @@ -0,0 +1,101 @@ +"""``tools/ci_linux_package_smoke.py``: every judge passes what a good bundle prints +and names each way a bad one was measured to go wrong (a smoke that cannot fail +proves nothing).""" +import os +import sys + +from beantester import desktop +from fakes import ROOT, check + +sys.path.insert(0, os.path.join(ROOT, "tools")) + +import ci_linux_package_smoke as smoke # noqa: E402 + +# xwininfo -root -tree lines of the window: ours, and Tk's default class (measured). +WINDOW = ' 0x2004b0 "Bean Network Tester": ("beannetworktester" "Beannetworktester") 1000x810+140+30' +TK_DEFAULT = ' 0x2004b0 "Bean Network Tester": ("tk" "Tk") 1000x810+140+30 +140+30' +PROGRAM = "/t/BeanNetworkTester/BeanNetworkTester" +ENTRY = f'[Desktop Entry]\nExec="{PROGRAM}"\nStartupWMClass=Beannetworktester\n' +GOOD_REPRO ="[bean] Reproduce: ~/work/BeanNetworkTester/BeanNetworkTester --duration 2 --seed 42 --simulate\n" + + +def test_a_good_bundle_passes_every_judge(): + check("codes", smoke.judge_codes(0, 0, 3) == []) + check("repro from ~", smoke.judge_repro(GOOD_REPRO) == []) + check("repro from its own folder", + smoke.judge_repro("Reproduce: ./BeanNetworkTester --simulate --seed 1") == []) + check("doctor", smoke.judge_doctor("user files: /t/config/BeanNetworkTester\n", "/t/config") == []) + check("window", smoke.judge_window(WINDOW, "") == []) + check("menu entry", smoke.judge_entry(0, ENTRY, "", PROGRAM, True) == []) + + +def test_every_wrong_exit_code_is_named(): + for codes, word in (((1, 0, 3), "--version"), ((0, 7, 3), "simulated"), ((0, 0, 1), "configuration")): + found = smoke.judge_codes(*codes) + check(f"{codes}: {word}", len(found) == 1 and word in found[0], f"({found})") + + +def test_every_wrong_repro_line_is_named(): + """It said ``BeanNetworkTester.exe`` on Linux before the package (measured).""" + for line, word in (("Reproduce: BeanNetworkTester.exe --simulate", "not from ~"), + ("Reproduce: sudo ~/b/BeanNetworkTester --simulate", "under sudo"), + ("Reproduce: python3 bean_network_tester.py --simulate", "not from ~"), + ("Reproduce: ~/b/BeanNetworkTester --seed 1", "lost --simulate"), + ("Finished: ok", "no Reproduce")): + found = smoke.judge_repro(line) + check(f"{line!r}: {word}", any(word in f for f in found), f"({found})") + + +def test_a_data_folder_elsewhere_is_named(): + found = smoke.judge_doctor("user files: /u/x/AppData/Local/BeanNetworkTester", "/t/config") + check("AppData is caught", len(found) == 1 and "AppData" in found[0], f"({found})") + check("no such line is caught", smoke.judge_doctor("OK python", "/t/config") != []) + + +def test_a_window_that_did_not_open_or_spoke_is_named(): + check("no window", "no window" in " ".join(smoke.judge_window(None, ""))) + check("Tk's default class", "class" in " ".join(smoke.judge_window(TK_DEFAULT, ""))) + for line in ('Fontconfig warning: "/etc/fonts/conf.d/48-guessfamily.conf", line 19', + "fc-match: symbol lookup error: fc-match: undefined symbol", + "No tkinter. Use CLI mode", "Traceback (most recent call last):"): + found = smoke.judge_window(WINDOW, line + "\n") + check(f"{line[:30]!r} is caught", len(found) == 1 and "printed 1 line" in found[0], f"({found})") + + +def test_every_wrong_menu_entry_is_named(): + for case, word in (((5, None, "", PROGRAM, True), "answered 5"), + ((0, ENTRY.replace(PROGRAM, "/elsewhere/B"), "", PROGRAM, True), "this program"), + ((0, ENTRY.replace("StartupWMClass=Beannetworktester", ""), "", PROGRAM, True), + "class"), + ((0, ENTRY, "x.desktop: error: value is not quoted\n", PROGRAM, True), + "desktop-file-validate"), + ((0, ENTRY, "", PROGRAM, False), "left the entry")): + found = smoke.judge_entry(*case) + check(word, any(word in f for f in found), f"({found})") + + +def test_the_validator_is_heard_by_its_exit_code_and_both_streams(): + """desktop-file-validate's exit code says whether it found an error, and it can + say so on either stream or on none; a warning comes with exit 0 and still counts, + because a clean entry here has neither.""" + check("a clean entry", smoke.validator_says(0, "", "") == "") + check("an error without a word: its exit code", + smoke.validator_says(1, "", "") == "exit 1 without a word") + check("an error on stderr", "error: bad" in smoke.validator_says(1, "", "error: bad\n")) + check("an error on stdout", "not quoted" in smoke.validator_says(1, "x: error: not quoted\n", "")) + check("a warning with exit 0", "hint" in smoke.validator_says(0, "x: hint: y\n", "")) + + +def test_it_looks_for_what_the_program_writes(): + """The smoke's own copies of the class and the file name (it runs on the bundle, + without the program's sources) are the program's.""" + check("the window's class", smoke.WINDOW_CLASS == desktop.WINDOW_CLASS) + check("the entry's file name", smoke.ENTRY_FILE == desktop.FILE_NAME) + + +def test_it_needs_a_display_and_one_program(monkeypatch, capsys): + check("usage", smoke.main([]) == 2) + monkeypatch.setattr(smoke.sys, "platform", "linux") + monkeypatch.delenv("DISPLAY", raising=False) + check("no display fails, never skips", smoke.main(["/b/BeanNetworkTester"]) == 1 + and "display" in capsys.readouterr().out) diff --git a/tests/test_cli_runtime.py b/tests/test_cli_runtime.py index e6ed75b4..5bac1490 100644 --- a/tests/test_cli_runtime.py +++ b/tests/test_cli_runtime.py @@ -12,6 +12,7 @@ import io import json import os +import sys import threading import time @@ -1732,12 +1733,20 @@ def test_duration_survives_the_repro_command(): def test_repro_command_follows_a_frozen_build(monkeypatch): - """A frozen user has no ``python bean_network_tester.py`` to paste.""" + """A frozen user has no ``python bean_network_tester.py`` to paste: the exe on + Windows, the Linux package's program under sudo on Linux.""" from beantester import DEFAULT_SETTINGS, appinfo, paths, repro monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(sys, "platform", "win32") cmd = repro.settings_to_cli_string(dict(DEFAULT_SETTINGS, loss=5)) check("repro: the frozen command is the exe", - cmd.startswith(appinfo.EXE_NAME), f"({cmd})") + cmd.startswith(appinfo.EXE_NAME + " "), f"({cmd})") + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setattr(appinfo, "_persons_home", lambda: "/u/ana") + monkeypatch.setattr(sys, "executable", "/u/ana/BeanNetworkTester/BeanNetworkTester") + cmd = repro.settings_to_cli_string(dict(DEFAULT_SETTINGS, loss=5)) + check("repro: on Linux the package's program, under sudo", + cmd.startswith("sudo ~/BeanNetworkTester/BeanNetworkTester --"), f"({cmd})") def test_the_saved_config_round_trips_through_the_cli(tmp_path): diff --git a/tests/test_crashlog.py b/tests/test_crashlog.py index e6ecbe1b..28f7d5c0 100644 --- a/tests/test_crashlog.py +++ b/tests/test_crashlog.py @@ -1223,7 +1223,7 @@ def test_a_tk_that_cannot_start_is_said_and_recorded(isolated, monkeypatch, caps left a traceback on a stderr the frozen build had already thrown away.""" from beantester import exitcodes - def no_display(): + def no_display(**_options): raise RuntimeError("no display name and no $DISPLAY environment variable") fake_tk = types.ModuleType("tkinter") @@ -1296,6 +1296,21 @@ def test_a_missing_tkinter_is_not_a_crash(isolated, monkeypatch, capsys): assert len(shown) == 1 and "No tkinter" in shown[0][1], shown +def test_a_library_of_the_system_tk_cannot_load_is_named(isolated, monkeypatch, capsys): + """Measured 2026-10-28: the Linux package on a system without libXft said "No + tkinter" - tkinter was there, a library of the system was not. The line names it, + and still offers the command line; it is the machine's business, not a crash.""" + cli, shown = _gui_start(monkeypatch, frozen=True) + missing = ImportError("libXft.so.2: cannot open shared object file: No such file or " + "directory", name="_tkinter") + cli._window_did_not_open(missing) + err = capsys.readouterr().err + assert "libXft.so.2" in err and "No tkinter" not in err, err + assert "--simulate" in err, err + assert _entries(isolated) == [], "a missing library was recorded as a crash" + assert len(shown) == 1 and "libXft.so.2" in shown[0][1], shown + + # -- what a repeat and a first record cost (performance review 2026-09-26) ----- # def _boom_elsewhere(): """A second fault from a different place than ``_boom``.""" diff --git a/tests/test_desktop.py b/tests/test_desktop.py new file mode 100644 index 00000000..68a44b47 --- /dev/null +++ b/tests/test_desktop.py @@ -0,0 +1,210 @@ +"""The Linux package's menu entry (``beantester/desktop.py``) and its two options. + +``--install-desktop`` writes one Desktop Entry for the person, ``--remove-desktop`` +takes it away (owner decision 2026-10-28). What can go wrong is in the file's +own rules - the Desktop Entry Specification quotes ``Exec`` arguments and then +escapes the value as a string, so a path with a quote, a dollar or a backslash is +easy to get wrong - and in where and as whom it is written. +""" +import os +import shutil +import subprocess +import sys +import types + +import pytest + +from beantester import appinfo, cli, desktop, exitcodes, linuxenv, paths, winenv +from fakes import check + +PROGRAM = "/u/ana/BeanNetworkTester/BeanNetworkTester" +ICON = "/u/ana/BeanNetworkTester/_internal/bean.png" + + +@pytest.fixture +def package(monkeypatch): + """This run as the Linux package, started by a person (no root).""" + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(paths, "running_as_root", lambda: False) + + +def _entry(tmp_path): + return tmp_path / ".local" / "share" / "applications" / desktop.FILE_NAME + + +def test_the_entry_goes_into_the_persons_applications_folder(): + say = desktop.applications_dir + check("XDG_DATA_HOME", say({"XDG_DATA_HOME": "/u/ana/.data", "HOME": "/u/ana"}) + == "/u/ana/.data/applications") + check("a relative one is ignored (XDG spec)", say({"XDG_DATA_HOME": "data", "HOME": "/u/ana"}) + == "/u/ana/.local/share/applications") + check("none: ~/.local/share", say({"HOME": "/u/ana"}) == "/u/ana/.local/share/applications") + + +def test_exec_quotes_the_path_the_way_the_specification_does(): + """Quoting first (``"`` ` ``$`` ``\\`` get a backslash), then the string escape + on top (each backslash doubled), and ``%`` as ``%%`` - the spec's own example: + a literal backslash in a quoted argument is four backslashes in the file.""" + check("a plain path", desktop.exec_value(PROGRAM) == f'"{PROGRAM}"') + check("a space needs only the quotes", + desktop.exec_value("/u/ana/My Apps/B") == '"/u/ana/My Apps/B"') + quoted = desktop.exec_value('/u/a"b/B') + check("a double quote", quoted == '"/u/a\\\\"b/B"', f"({quoted})") + check("a dollar and a backtick", desktop.exec_value("/u/$x`y`/B") == '"/u/\\\\$x\\\\`y\\\\`/B"') + check("a backslash: four", desktop.exec_value("/u/a\\b/B") == '"/u/a\\\\\\\\b/B"') + check("a percent sign is not a field code", desktop.exec_value("/u/100%/B") == '"/u/100%%/B"') + + +def test_the_entry_starts_the_program_itself_and_names_the_windows_class(): + text = desktop.entry_text(PROGRAM, ICON) + lines = text.splitlines() + check("a Desktop Entry", lines[0] == "[Desktop Entry]" and "Type=Application" in lines) + check("the program, not pkexec (the window asks for root itself)", + f'Exec="{PROGRAM}"' in lines and "pkexec" not in text) + check("its icon", f"Icon={ICON}" in lines) + check("the window's class, so the dock shows the entry's name and icon", + f"StartupWMClass={desktop.WINDOW_CLASS}" in lines) + check("no terminal", "Terminal=false" in lines) + check("named as the window is", f"Name={appinfo.APP_NAME}" in lines) + + +@pytest.mark.skipif(shutil.which("desktop-file-validate") is None, + reason="desktop-file-validate is not installed") +def test_the_entry_is_valid_for_desktop_file_validate(tmp_path): + entry = tmp_path / desktop.FILE_NAME + entry.write_text(desktop.entry_text('/u/a "b"/$x/B', ICON), encoding="utf-8") + done = subprocess.run(["desktop-file-validate", str(entry)], capture_output=True, text=True) + check("no errors, no warnings", done.returncode == 0 and not done.stdout.strip(), + f"({done.stdout}{done.stderr})") + + +def test_only_the_linux_package_run_by_a_person_writes_it(monkeypatch, package): + check("the package, a person: yes", desktop.why_not() is None) + monkeypatch.setattr(paths, "running_as_root", lambda: True) + check("under sudo: no - the file would be root's", "without sudo" in desktop.why_not()) + monkeypatch.setattr(paths, "running_as_root", lambda: False) + monkeypatch.setattr(paths, "is_frozen", lambda: False) + check("from sources: no", "from sources" in desktop.why_not()) + monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(sys, "platform", "win32") + check("not Linux: no", "not Linux" in desktop.why_not()) + + +def test_install_writes_the_entry_and_writes_it_again(tmp_path, package): + env = {"HOME": str(tmp_path)} + path, error = desktop.install(PROGRAM, ICON, env) + entry = _entry(tmp_path) + check("written where the menu reads it", error is None and os.path.samefile(path, entry), + f"({path}, {error})") + check("with the program", f'Exec="{PROGRAM}"' in entry.read_text(encoding="utf-8")) + if os.name == "posix": + check("executable by nobody", not entry.stat().st_mode & 0o111) + desktop.install(PROGRAM.replace("ana", "ana/new"), ICON, env) + check("a second install replaces it (the folder moved)", + "ana/new" in entry.read_text(encoding="utf-8")) + check("and leaves no temporary file", + sorted(os.listdir(entry.parent)) == [desktop.FILE_NAME]) + + +def test_a_path_the_entry_cannot_carry_is_refused(tmp_path, package): + """A control character has no encoding in the file, and the program's path may + not hold "=" at all (the spec's Exec key); an icon's path is a plain string.""" + env = {"HOME": str(tmp_path)} + path, error = desktop.install("/u/ana/a\nb/B", ICON, env) + check("a control character: refused, nothing written", path is None + and "control character" in error and not _entry(tmp_path).exists(), f"({error})") + path, error = desktop.install("/u/ana/a=b/BeanNetworkTester", ICON, env) + check("'=' in the program's path: refused, and what to do", path is None + and "'='" in error and "move the folder" in error and not _entry(tmp_path).exists(), + f"({error})") + path, error = desktop.install(PROGRAM, "/u/ana/a=b/bean.png", env) + check("'=' in the icon's path is written", error is None and path, f"({error})") + + +def test_a_folder_that_cannot_be_written_is_said_and_leaves_nothing(tmp_path, package): + (tmp_path / ".local").write_text("a file where the folder would be", encoding="utf-8") + path, error = desktop.install(PROGRAM, ICON, {"HOME": str(tmp_path)}) + check("not written, and why", path is None and error.startswith("cannot write"), f"({error})") + + +def test_a_failed_write_takes_away_its_own_temporary_file_only(tmp_path, monkeypatch, package): + """Two installs at once each write a temporary file of their own: one name for + both (".tmp") let one truncate what the other was writing, and a failed + one removes only the file it made.""" + folder = _entry(tmp_path).parent + folder.mkdir(parents=True) + other = folder / (desktop.FILE_NAME + ".tmp") + other.write_text("half of another install's entry", encoding="utf-8") + + def full(_source, _target): + raise OSError(28, "No space left on device") + + monkeypatch.setattr(desktop.os, "replace", full) + path, error = desktop.install(PROGRAM, ICON, {"HOME": str(tmp_path)}) + check("not written, and why", path is None and "No space left" in error, f"({error})") + check("its own temporary file is gone, the other install's is untouched", + sorted(os.listdir(folder)) == [other.name] + and other.read_text(encoding="utf-8") == "half of another install's entry", + f"({sorted(os.listdir(folder))})") + + +def test_remove_takes_only_its_own_file(tmp_path, package): + env = {"HOME": str(tmp_path)} + desktop.install(PROGRAM, ICON, env) + other = _entry(tmp_path).parent / "org.example.Other.desktop" + other.write_text("[Desktop Entry]\n", encoding="utf-8") + path, error = desktop.remove(env) + check("removed", error is None and path and not _entry(tmp_path).exists()) + check("another program's entry stays", other.exists()) + check("nothing to remove: said, not an error", desktop.remove(env) == (None, None)) + + +def test_the_options_answer_with_exit_codes(monkeypatch, capsys): + monkeypatch.setattr(desktop, "why_not", lambda: "the menu entry is for the Linux package") + check("not here: 1 and why", cli.run_cli(["--install-desktop"]) == exitcodes.RUNTIME + and "Linux package" in capsys.readouterr().err) + monkeypatch.setattr(desktop, "why_not", lambda: None) + monkeypatch.setattr(desktop, "install", lambda: (None, "cannot write /x: Permission denied")) + check("not written: 5", cli.run_cli(["--install-desktop"]) == exitcodes.IO) + monkeypatch.setattr(desktop, "install", lambda: ("/u/ana/x.desktop", None)) + check("written: 0, and where", cli.run_cli(["--install-desktop"]) == exitcodes.OK + and "/u/ana/x.desktop" in "".join(capsys.readouterr())) + monkeypatch.setattr(desktop, "remove", lambda: (None, None)) + check("nothing to remove: 0", cli.run_cli(["--remove-desktop"]) == exitcodes.OK + and "no menu entry" in "".join(capsys.readouterr())) + + +def test_install_and_remove_together_is_a_usage_error(capsys): + with pytest.raises(SystemExit) as stop: + cli.build_arg_parser().parse_args(["--install-desktop", "--remove-desktop"]) + check("argparse refuses it", stop.value.code == exitcodes.USAGE) + capsys.readouterr() + + +def test_the_class_is_written_the_way_tk_gives_it(): + """Tk title-cases the class it is given (measured: "BeanNetworkTester" became + "Beannetworktester"), so a class in any other form is one no window has - and + it is the program's own, not Tk's default "Tk", which every Tk program shares + and GNOME matches to no entry.""" + check("the class measured on the package's window", + desktop.WINDOW_CLASS == "Beannetworktester", f"({desktop.WINDOW_CLASS})") + + +def test_the_window_carries_the_class_the_entry_names(monkeypatch): + """Tk's default class is "Tk" (measured with xwininfo on the package); the + window's root is made with the class StartupWMClass names.""" + made = [] + fake_tk = types.ModuleType("tkinter") + fake_tk.Tk = lambda **options: made.append(options) or types.SimpleNamespace( # type: ignore[attr-defined] + mainloop=lambda: None) + fake_gui = types.ModuleType("beantester.gui") + fake_gui.App = lambda root: None # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, "tkinter", fake_tk) + monkeypatch.setitem(sys.modules, "beantester.gui", fake_gui) + monkeypatch.setattr(winenv, "is_windows", lambda: False) + monkeypatch.setattr(linuxenv, "wants_root", lambda: False) + monkeypatch.setattr(linuxenv, "take_desktop", lambda: []) + monkeypatch.setattr(cli.crashlog, "arm_native", lambda: None) + check("opened", cli._run_gui([]) == exitcodes.OK) + check("with the entry's class", made == [{"className": desktop.WINDOW_CLASS}], f"({made})") diff --git a/tests/test_license_surface.py b/tests/test_license_surface.py index fdf9014e..83903e0d 100644 --- a/tests/test_license_surface.py +++ b/tests/test_license_surface.py @@ -15,6 +15,7 @@ """ import io import json +import sys from beantester import appinfo, exitcodes, legal from beantester.cli import run_cli @@ -47,7 +48,8 @@ def test_every_shipped_component_is_named_with_a_version_and_a_source(): the user was given, so they can fetch that source and replace it.""" rows = legal.component_rows() names = [name for name, *_ in rows] - for required in ("WinDivert", "PyDivert", "psutil", "Python"): + divert = "eBPFDivert" if sys.platform.startswith("linux") else "WinDivert" + for required in (divert, "PyDivert", "psutil", "Python"): check(f"{required} is declared", required in names, f"({names})") for name, version, licence, url in rows: check(f"{name} carries a version", bool(str(version).strip())) @@ -284,9 +286,14 @@ def test_the_written_offer_lasts_as_long_as_the_licence_demands(): has to be at least what the licence requires. """ notices = legal.notices_text() - check("notices: the written offer names the three-year floor", - "three years" in notices, - "(an offer weaker than GPLv3 section 6(b) allows)") + offers = notices.split("**Written offer:**")[1:] + # PyDivert's, and eBPFDivert's for the Linux package (2026-10-28): EACH one, or a + # weaker second offer would hide behind the first one's words. + check("notices: both written offers are there", len(offers) == 2, f"({len(offers)})") + for offer in offers: + check("notices: the written offer names the three-year floor", + "three years" in offer.split("\n\n")[0], + "(an offer weaker than GPLv3 section 6(b) allows)") def test_the_shipped_driver_is_byte_for_byte_the_one_we_recorded(): @@ -326,3 +333,59 @@ def test_the_shipped_driver_is_byte_for_byte_the_one_we_recorded(): digest == recorded.get(name), f"(got {digest[:16]}..., recorded {str(recorded.get(name))[:16]}... - " f"if the pydivert pin moved, re-record it deliberately)") + + +def test_both_builds_are_in_the_registry_and_the_notices(): + """The Windows zip and the Linux .tar.gz carry different libraries (2026-10-28), + and the test above only sees the one this system builds - so both lists are held + to the notices here, wherever the suite runs.""" + import os + from fakes import ROOT + + notices = open(os.path.join(ROOT, "THIRD-PARTY-NOTICES.md"), encoding="utf-8").read() + for platform in ("win32", "linux"): + for name, *_ in legal.components(platform): + stem = name.split(" (")[0] + check(f"{platform}: the notices have a section for {stem}", + ("## " + stem) in notices) + linux = {name for name, *_ in legal.components("linux")} + windows = {name for name, *_ in legal.components("win32")} + check("Linux carries eBPFDivert, not WinDivert", + "eBPFDivert" in linux and "WinDivert" not in linux and "WinDivert" in windows) + check("the Windows runtime is not claimed for Linux", + not linux & {"Microsoft C Runtime", "libtommath"}) + check("this system's list is the one --license reports", + legal.COMPONENTS == legal.components()) + + +def test_the_parts_of_ebpfdivert_are_named_with_the_versions_read(): + """libebpfdivert.so carries libbpf, libelf and Zstandard built from their own + sources, so the notices name the versions eBPFDivert's release script builds - + not the ones a distribution packages (they named Ubuntu 24.04's at first) - and + --license and the SBOM report the same.""" + import os + from fakes import ROOT + + notices = open(os.path.join(ROOT, "THIRD-PARTY-NOTICES.md"), encoding="utf-8").read() + check("the notices say which eBPFDivert the parts were read for", + f"eBPFDivert {legal.EBPFDIVERT_READ} carries" in notices) + rows = {name: version for name, version, _lic, _url in legal.component_rows("linux")} + for name, version in legal.EBPFDIVERT_PARTS.items(): + stem = name.split(" (")[0] + check(f"the notices name {stem} {version}", f"{stem} {version}" in notices) + check(f"--license reports {stem} {version}", rows.get(name) == version, f"({rows.get(name)})") + + +def test_the_parts_were_read_for_the_ebpfdivert_we_ship(): + """The other half: the eBPFDivert in the installed pydivert is the one the + parts were read for, the way the WinDivert test above holds the driver. A + pydivert that brings another eBPFDivert lands here, on the Linux runner, until + somebody reads its release script again (legal.EBPFDIVERT_PARTS).""" + import sys as _sys + + shipped = legal._ebpfdivert_version() + if shipped == "-" or not _sys.platform.startswith("linux"): + return # Windows, or no pydivert library here + check("the eBPFDivert pydivert brings is the one the parts were read for", + shipped == legal.EBPFDIVERT_READ, + f"(pydivert brings {shipped!r}, legal.EBPFDIVERT_READ is {legal.EBPFDIVERT_READ!r})") diff --git a/tests/test_linux_bundle.py b/tests/test_linux_bundle.py new file mode 100644 index 00000000..58ad3938 --- /dev/null +++ b/tests/test_linux_bundle.py @@ -0,0 +1,82 @@ +"""``tools/linux_bundle.py``: the libraries the Linux bundle takes from the system, +and the check CI runs on the bundle it built. The check is proven able to fail on +each thing it looks for (a tool that cannot fail proves nothing).""" +import os +import sys + +from fakes import ROOT, check + +sys.path.insert(0, os.path.join(ROOT, "tools")) + +import linux_bundle # noqa: E402 + +GOOD = ("_internal/libtk8.6.so", "_internal/libXft.so.2", "_internal/libX11.so.6", + "_internal/pydivert/bpf/libebpfdivert.so", + "_internal/python3.15/lib-dynload/_tkinter.cpython-315-x86_64-linux-gnu.so") + + +def _bundle(tmp_path, names): + for name in names: + path = tmp_path / "BeanNetworkTester" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"\x7fELF") + return str(tmp_path / "BeanNetworkTester") + + +def test_a_bundle_that_keeps_to_the_list_passes(tmp_path, capsys): + folder = _bundle(tmp_path, GOOD) + check("nothing wrong", linux_bundle.problems(folder) == [], f"({linux_bundle.problems(folder)})") + check("and the command says so", linux_bundle.main(["check", folder]) == 0 + and "none of them the system's own" in capsys.readouterr().out) + + +def test_a_bundle_carrying_the_systems_font_stack_fails(tmp_path): + """Measured: a bundled fontconfig 2.15 rejects Arch's 2.18 configuration.""" + for name in ("libfontconfig.so.1", "libfreetype.so.6", "libexpat.so.1", "libpng16.so.16", + "libbrotlicommon.so.1", "libbrotlidec.so.1", "libbz2.so.1.0"): + folder = _bundle(tmp_path / name, GOOD + (f"_internal/{name}",)) + found = linux_bundle.problems(folder) + check(f"{name} is caught", len(found) == 1 and name in found[0], f"({found})") + check(f"{name}: the command fails", linux_bundle.main(["check", folder]) == 1) + + +def test_what_the_systems_libxcb_brings_is_not_carried(tmp_path): + """The system's libxcb needs libXau and libXdmcp (measured), so they are there.""" + for name in ("libXau.so.6", "libXdmcp.so.6", "libbsd.so.0", "libmd.so.0"): + found = linux_bundle.problems(_bundle(tmp_path / name, GOOD + (f"_internal/{name}",))) + check(f"{name} is caught", len(found) == 1 and "from the system" in found[0], f"({found})") + + +def test_modules_the_program_never_uses_are_not_carried(tmp_path): + for name in ("python3.15/lib-dynload/_bz2.cpython-315-x86_64-linux-gnu.so", + "python3.15/lib-dynload/_lzma.cpython-315-x86_64-linux-gnu.so", + "python3.15/lib-dynload/_zstd.cpython-315-x86_64-linux-gnu.so", + "liblzma.so.5", "libzstd.so.1", "libBLT.2.5.so.8.6"): + found = linux_bundle.problems(_bundle(tmp_path / name.replace("/", "_"), + GOOD + (f"_internal/{name}",))) + check(f"{name} is caught", len(found) == 1 and "never uses" in found[0], f"({found})") + check("a module that only starts like one stays", + not linux_bundle.not_needed("_bz2x.cpython-315.so")) + + +def test_a_bundle_missing_what_it_must_carry_fails(tmp_path): + """Measured: without libXft, a system that lacks it opens no window.""" + # Written out, not read from MUST_CARRY: a part dropped from the list would + # otherwise drop out of this test too (a mutation measured exactly that). + for part in ("libtk", "libXft.so", "libebpfdivert.so"): + folder = _bundle(tmp_path / part, [n for n in GOOD if part not in n]) + found = linux_bundle.problems(folder) + check(f"no {part}: caught", found == [f"does not carry {part}"], f"({found})") + check("no folder at all fails", linux_bundle.problems(str(tmp_path / "none")) != []) + + +def test_libraries_the_bundle_keeps_are_not_mistaken_for_the_systems(): + for name in ("libXft.so.2", "libX11.so.6", "libpython3.15.so.1.0", "libz.so.1", + "libffi.so.8"): + check(f"{name} stays", not linux_bundle.taken_from_system(name)) + check("a path is judged by its file name", + linux_bundle.taken_from_system("/x/_internal/libfreetype.so.6")) + + +def test_a_wrong_command_line_is_refused(capsys): + check("usage", linux_bundle.main([]) == 2 and "usage" in capsys.readouterr().err) diff --git a/tests/test_linuxenv.py b/tests/test_linuxenv.py index ba69ae22..47267f41 100644 --- a/tests/test_linuxenv.py +++ b/tests/test_linuxenv.py @@ -310,7 +310,7 @@ def _fake_tk(tk_fails): class TclError(Exception): pass - def tk(): + def tk(**_options): if tk_fails: raise TclError("no display name and no $DISPLAY environment variable") return types.SimpleNamespace(mainloop=lambda: None) @@ -371,3 +371,45 @@ def test_a_copy_that_cannot_reach_the_screen_hands_the_window_back(monkeypatch): _gui(monkeypatch, tk_fails=False, app_fails=True) monkeypatch.setattr(linuxenv, "started_by_pkexec", lambda: True) check("a fault in the window itself is not handed back", cli._run_gui([]) == exitcodes.RUNTIME) + + +# -- the frozen build's library path ---------------------------------------------- # +def test_the_programs_the_package_starts_get_the_persons_library_path(monkeypatch): + """The bootloader points LD_LIBRARY_PATH at the bundle; a system program that + inherits it loads the bundle's older libraries and stops (measured 2026-10-28: + "fc-match: symbol lookup error" on Ubuntu 26.04, Fedora 44 and Arch). The value + the person had comes back, or none when they had none.""" + monkeypatch.setattr(linuxenv, "is_linux", lambda: True) + monkeypatch.setattr(winenv, "is_frozen", lambda: True) + env = {"LD_LIBRARY_PATH": "/b/_internal", "LD_LIBRARY_PATH_ORIG": "/opt/cuda/lib64", "X": "1"} + check("their own value back", linuxenv.restore_library_path(env) is True + and env == {"LD_LIBRARY_PATH": "/opt/cuda/lib64", "X": "1"}, f"({env})") + env = {"LD_LIBRARY_PATH": "/b/_internal", "X": "1"} + linuxenv.restore_library_path(env) + check("none of their own: none at all", env == {"X": "1"}, f"({env})") + env = {"LD_LIBRARY_PATH": "/b/_internal", "LD_LIBRARY_PATH_ORIG": ""} + linuxenv.restore_library_path(env) + check("an empty one is none (an empty entry would mean the working folder)", env == {}, + f"({env})") + + +def test_from_sources_and_on_windows_the_library_path_is_left_alone(monkeypatch): + for linux, frozen in ((True, False), (False, True)): + monkeypatch.setattr(linuxenv, "is_linux", lambda linux=linux: linux) + monkeypatch.setattr(winenv, "is_frozen", lambda frozen=frozen: frozen) + env = {"LD_LIBRARY_PATH": "/u/ana/lib", "LD_LIBRARY_PATH_ORIG": "/x"} + check(f"linux={linux}, frozen={frozen}: untouched", + linuxenv.restore_library_path(env) is False + and env == {"LD_LIBRARY_PATH": "/u/ana/lib", "LD_LIBRARY_PATH_ORIG": "/x"}) + + +def test_the_library_path_is_restored_before_the_program_does_anything(monkeypatch): + done = [] + monkeypatch.setattr(crashlog, "install", lambda: done.append("crashlog")) + monkeypatch.setattr(linuxenv, "restore_library_path", lambda: done.append("library path")) + monkeypatch.setattr(cli, "run_cli", lambda argv: done.append("cli") or 0) + monkeypatch.setattr(cli, "_run_gui", lambda argv, relaunched: done.append("gui") or 0) + cli.main(["--version"]) + cli.main([]) + check("before the command line and before the window", + done == ["crashlog", "library path", "cli", "crashlog", "library path", "gui"], f"({done})") diff --git a/tests/test_mutation_registry.py b/tests/test_mutation_registry.py index b9c334f0..07f29148 100644 --- a/tests/test_mutation_registry.py +++ b/tests/test_mutation_registry.py @@ -284,8 +284,8 @@ # no PyInstaller to ask, and the answer there must stay "no assertion". "label": "sbom: an absent build tool gets an invented version", "file": "beantester/legal.py", - "old": ' return "bundled"\n\n\ndef component_rows():', - "new": ' return "0.0.0"\n\n\ndef component_rows():', + "old": ' return "bundled"\n\n\ndef _ebpfdivert_version():', + "new": ' return "0.0.0"\n\n\ndef _ebpfdivert_version():', "test": "test_a_build_tool_that_is_not_installed_is_not_invented", }, { @@ -992,8 +992,8 @@ # the notice itself exists to cure, one level up. "label": "cron notice: a job drops out of what the weekly notice watches", "file": ".github/workflows/ci.yml", - "old": " needs: [public-text, lint, types, semgrep, audit, mutations, tests, linux-capture, build]", - "new": " needs: [public-text, lint, types, semgrep, audit, mutations, tests, build]", + "old": " needs: [public-text, lint, types, semgrep, audit, mutations, tests, linux-capture, build, build-linux]", + "new": " needs: [public-text, lint, types, semgrep, audit, mutations, tests, linux-capture, build]", "test": "test_the_cron_notice_watches_every_job_in_the_workflow", }, { @@ -1743,15 +1743,30 @@ { "label": "licence: the notices name a WinDivert file that is not shipped", "file": "THIRD-PARTY-NOTICES.md", - "old": "## WinDivert (`WinDivert64.dll`, `WinDivert64.sys`)", - "new": "## WinDivert (`WinDivert.dll`, `WinDivert64.sys`)", + "old": "## WinDivert (`WinDivert64.dll`, `WinDivert64.sys`, the Windows zip)", + "new": "## WinDivert (`WinDivert.dll`, `WinDivert64.sys`, the Windows zip)", "test": "test_the_notices_name_the_windivert_files_that_are_really_shipped", }, { "label": "licence: the written offer drops below the three-year floor", "file": "THIRD-PARTY-NOTICES.md", - "old": "**Written offer:** for at least three years from the date of this release, the", - "new": "**Written offer:** for as long as this release is distributed, the", + "old": "**Written offer:** for at least three years from the date of this release, the\n" + "Author will supply, on request and at no charge beyond the cost of delivery, the\n" + "complete corresponding source of the PyDivert", + "new": "**Written offer:** for as long as this release is distributed, the\n" + "Author will supply, on request and at no charge beyond the cost of delivery, the\n" + "complete corresponding source of the PyDivert", + "test": "test_the_written_offer_lasts_as_long_as_the_licence_demands", + }, + { + "label": "licence: the Linux package's written offer drops below the floor", + "file": "THIRD-PARTY-NOTICES.md", + "old": "**Written offer:** for at least three years from the date of this release, the\n" + "Author will supply, on request and at no charge beyond the cost of delivery, the\n" + "complete corresponding source of the eBPFDivert", + "new": "**Written offer:** for as long as this release is distributed, the\n" + "Author will supply, on request and at no charge beyond the cost of delivery, the\n" + "complete corresponding source of the eBPFDivert", "test": "test_the_written_offer_lasts_as_long_as_the_licence_demands", }, { @@ -7407,7 +7422,9 @@ "label": "cli: a Tk that cannot start escapes the window guard", "file": "beantester/cli.py", "old": " winenv.set_dpi_awareness() # before the Tk root exists\n" - " root = tk.Tk()\n" + " # The class the Linux menu entry names (StartupWMClass): Tk's default \"Tk\"\n" + " # matches no entry, and GNOME shows such a window as a program of its own.\n" + " root = tk.Tk(className=desktop.WINDOW_CLASS)\n" " App(root)\n" " except Exception as exc:\n" " code = _window_did_not_open(exc)\n" @@ -7419,7 +7436,7 @@ "new": " except Exception as exc:\n" " return _window_did_not_open(exc)\n" " winenv.set_dpi_awareness()\n" - " root = tk.Tk()\n" + " root = tk.Tk(className=desktop.WINDOW_CLASS)\n" " App(root)\n", "test": "test_a_tk_that_cannot_start_is_said_and_recorded", }, @@ -10070,6 +10087,367 @@ "new": "", "test": "test_a_pkexec_that_cannot_be_started_is_recorded_and_said", }, + # -- the Linux package (.tar.gz, 2026-10-28) ------------------------------------ # + { + "label": "linux package: the data folder stays ~/AppData/Local", + "file": "beantester/paths.py", + "old": " if sys.platform.startswith(\"linux\"):\n return posixpath.join(config_home(), TOOL_ID)\n", + "new": "", + "test": "test_the_linux_package_keeps_the_files_in_the_persons_config_folder", + }, + { + "label": "config folder: a relative XDG_CONFIG_HOME is taken", + "file": "beantester/paths.py", + "old": " if posixpath.isabs(value):\n", + "new": " if value:\n", + "test": "test_the_config_folder_is_the_xdg_one_and_the_persons_under_root", + }, + { + "label": "config folder: root's for a person, not theirs", + "file": "beantester/paths.py", + "old": " return posixpath.join(found, \".config\")\n", + "new": " pass\n", + "test": "test_the_config_folder_is_the_xdg_one_and_the_persons_under_root", + }, + { + "label": "linux package: files beside the program are adopted", + "file": "beantester/paths.py", + "old": " if not explicit and sys.platform != \"win32\":\n", + "new": " if False:\n", + "test": "test_the_linux_package_adopts_nothing_beside_itself", + }, + { + "label": "linux package: the command starts an interpreter", + "file": "beantester/appinfo.py", + "old": " if paths.is_frozen() if frozen is None else frozen:\n", + "new": " if False:\n", + "test": "test_the_linux_package_names_its_own_program", + }, + { + "label": "linux package: outside the home the command names a path", + "file": "beantester/appinfo.py", + "old": "or \"./\" + LINUX_EXE_NAME", + "new": "or executable", + "test": "test_the_linux_package_names_its_own_program", + }, + { + "label": "linux package: a capture without sudo", + "file": "beantester/appinfo.py", + "old": " return \" \".join(([] if simulate else [\"sudo\"]) + [program])\n", + "new": " return program\n", + "test": "test_the_linux_package_names_its_own_program", + }, + { + "label": "linux package: the command is the .exe", + "file": "beantester/appinfo.py", + "old": " if sys.platform.startswith(\"linux\"):\n return linux_command(simulate)\n if is_frozen():\n", + "new": " if is_frozen():\n return EXE_NAME\n if sys.platform.startswith(\"linux\"):\n return linux_command(simulate)\n if is_frozen():\n", + "test": "test_the_frozen_command_follows_the_system_it_runs_on", + }, + { + "label": "linux package: argparse names the .exe", + "file": "beantester/appinfo.py", + "old": " return exe_name() if is_frozen() else LAUNCHER", + "new": " return EXE_NAME if is_frozen() else LAUNCHER", + "test": "test_the_frozen_command_follows_the_system_it_runs_on", + }, + { + "label": "library path: the person's own value is dropped", + "file": "beantester/linuxenv.py", + "old": " if original:\n environ[LIBRARY_PATH] = original\n else:\n environ.pop(LIBRARY_PATH, None)\n", + "new": " environ.pop(LIBRARY_PATH, None)\n", + "test": "test_the_programs_the_package_starts_get_the_persons_library_path", + }, + { + "label": "library path: the bundle's is left to the programs it starts", + "file": "beantester/linuxenv.py", + "old": " else:\n environ.pop(LIBRARY_PATH, None)\n return True\n", + "new": " return True\n", + "test": "test_the_programs_the_package_starts_get_the_persons_library_path", + }, + { + "label": "library path: the saved copy stays behind", + "file": "beantester/linuxenv.py", + "old": " original = environ.pop(LIBRARY_PATH_ORIG, \"\")\n", + "new": " original = environ.get(LIBRARY_PATH_ORIG, \"\")\n", + "test": "test_the_programs_the_package_starts_get_the_persons_library_path", + }, + { + "label": "library path: changed from sources too", + "file": "beantester/linuxenv.py", + "old": " if not (is_linux() and winenv.is_frozen()):\n return False\n original", + "new": " if not is_linux():\n return False\n original", + "test": "test_from_sources_and_on_windows_the_library_path_is_left_alone", + }, + { + "label": "library path: not restored when the program starts", + "file": "beantester/cli.py", + "old": " linuxenv.restore_library_path() # the frozen build's: not for the programs it starts\n", + "new": "", + "test": "test_the_library_path_is_restored_before_the_program_does_anything", + }, + { + "label": "a missing library of the system is called No tkinter", + "file": "beantester/cli.py", + "old": " if missing:\n", + "new": " if False:\n", + "test": "test_a_library_of_the_system_tk_cannot_load_is_named", + }, + { + "label": "linux bundle: a bundled fontconfig passes", + "file": "tools/linux_bundle.py", + "old": "FROM_SYSTEM = (\"libfontconfig.so\", ", + "new": "FROM_SYSTEM = (", + "test": "test_a_bundle_carrying_the_systems_font_stack_fails", + }, + { + "label": "linux bundle: a bundle without Xft passes", + "file": "tools/linux_bundle.py", + "old": "MUST_CARRY = (\"libtk\", \"libXft.so\", ", + "new": "MUST_CARRY = (\"libtk\", ", + "test": "test_a_bundle_missing_what_it_must_carry_fails", + }, + { + "label": "linux bundle: the check never fails the build", + "file": "tools/linux_bundle.py", + "old": " return 1 if found else 0\n", + "new": " return 0\n", + "test": "test_a_bundle_carrying_the_systems_font_stack_fails", + }, + { + "label": "package smoke: a simulation under sudo passes", + "file": "tools/ci_linux_package_smoke.py", + "old": " if program == \"sudo\":\n", + "new": " if False:\n", + "test": "test_every_wrong_repro_line_is_named", + }, + { + "label": "package smoke: fontconfig's warnings pass", + "file": "tools/ci_linux_package_smoke.py", + "old": "NOISE = (\"Fontconfig\", ", + "new": "NOISE = (", + "test": "test_a_window_that_did_not_open_or_spoke_is_named", + }, + { + "label": "package smoke: any data folder passes", + "file": "tools/ci_linux_package_smoke.py", + "old": " if want not in output:\n", + "new": " if False:\n", + "test": "test_a_data_folder_elsewhere_is_named", + }, + { + "label": "capture smoke: --program is not what the sessions run", + "file": "tools/ci_linux_capture_smoke.py", + "old": " if PROGRAM:\n return [PROGRAM, *args]\n", + "new": "", + "test": "test_the_sessions_run_the_program_they_are_given", + }, + { + "label": "capture smoke: a bundle run goes on into the tools' phases", + "file": "tools/ci_linux_capture_smoke.py", + "old": " if PROGRAM:\n return _report(failures)\n", + "new": "", + "test": "test_a_bundle_run_stops_after_the_sessions", + }, + # -- the Linux package's licences (legal.py, sbom.py, 2026-10-28) ---------------- # + { + "label": "licence: the Linux package reports the Windows list", + "file": "beantester/legal.py", + "old": " return LINUX_COMPONENTS if platform.startswith(\"linux\") else WINDOWS_COMPONENTS\n", + "new": " return WINDOWS_COMPONENTS\n", + "test": "test_both_builds_are_in_the_registry_and_the_notices", + }, + { + "label": "licence: the parts of libebpfdivert are read for another eBPFDivert", + "file": "beantester/legal.py", + "old": "EBPFDIVERT_READ = \"0.0.5\"\n", + "new": "EBPFDIVERT_READ = \"0.0.6\"\n", + "test": "test_the_parts_of_ebpfdivert_are_named_with_the_versions_read", + }, + { + "label": "licence: --license says bundled for the parts of libebpfdivert", + "file": "beantester/legal.py", + "old": " version = EBPFDIVERT_PARTS[name]\n", + "new": " version = \"bundled\"\n", + "test": "test_the_parts_of_ebpfdivert_are_named_with_the_versions_read", + }, + { + "label": "sbom: the two documents of a release share a namespace", + "file": "tools/sbom.py", + "old": " seed += \"|linux\"\n", + "new": " pass\n", + "test": "test_the_linux_package_has_an_sbom_of_its_own", + }, + { + "label": "sbom: the Linux document describes the Windows build", + "file": "tools/sbom.py", + "old": " shipped = legal.components(platform)\n", + "new": " shipped = legal.components(\"win32\")\n", + "test": "test_the_linux_package_has_an_sbom_of_its_own", + }, + { + "label": "sbom: the X.Org libraries' reference has no text", + "file": "tools/sbom.py", + "old": " \"LicenseRef-X.Org-libraries\":\n", + "new": " \"LicenseRef-X.Org-libraries-text\":\n", + "test": "test_the_linux_package_has_an_sbom_of_its_own", + }, + # -- the Linux package's menu entry (desktop.py, 2026-10-28) -------------------- # + { + "label": "menu entry: Exec is not quoted", + "file": "beantester/desktop.py", + "old": "if c in _EXEC_ESCAPED else c for c in program)", + "new": "if False else c for c in program)", + "test": "test_exec_quotes_the_path_the_way_the_specification_does", + }, + { + # The string escape on top of the quoting (the spec's four backslashes). + "label": "menu entry: Exec's backslashes are not doubled", + "file": "beantester/desktop.py", + "old": r'.replace("\\", "\\\\").replace("%", "%%")', + "new": r'.replace("%", "%%")', + "test": "test_exec_quotes_the_path_the_way_the_specification_does", + }, + { + "label": "menu entry: a percent sign reads as a field code", + "file": "beantester/desktop.py", + "old": '.replace("%", "%%")', + "new": "", + "test": "test_exec_quotes_the_path_the_way_the_specification_does", + }, + { + "label": "menu entry: no StartupWMClass", + "file": "beantester/desktop.py", + "old": " f\"StartupWMClass={WINDOW_CLASS}\",\n", + "new": "", + "test": "test_the_entry_starts_the_program_itself_and_names_the_windows_class", + }, + { + "label": "menu entry: the class is not in Tk's form", + "file": "beantester/desktop.py", + "old": "WINDOW_CLASS = \"Beannetworktester\"", + "new": "WINDOW_CLASS = \"BeanNetworkTester\"", + "test": "test_the_class_is_written_the_way_tk_gives_it", + }, + { + "label": "menu entry: written under sudo", + "file": "beantester/desktop.py", + "old": " if paths.running_as_root():\n", + "new": " if False:\n", + "test": "test_only_the_linux_package_run_by_a_person_writes_it", + }, + { + "label": "menu entry: written from sources", + "file": "beantester/desktop.py", + "old": " if not paths.is_frozen():\n", + "new": " if False:\n", + "test": "test_only_the_linux_package_run_by_a_person_writes_it", + }, + { + "label": "menu entry: a control character is written into the file", + "file": "beantester/desktop.py", + "old": " if _refused(path):\n", + "new": " if False:\n", + "test": "test_a_path_the_entry_cannot_carry_is_refused", + }, + { + "label": "menu entry: a program path with '=' is written", + "file": "beantester/desktop.py", + "old": " if \"=\" in program:\n", + "new": " if False:\n", + "test": "test_a_path_the_entry_cannot_carry_is_refused", + }, + { + "label": "menu entry: two installs share one temporary file", + "file": "beantester/desktop.py", + "old": " temporary = paths.temp_beside(target)\n", + "new": " temporary = target + \".tmp\"\n", + "test": "test_a_failed_write_takes_away_its_own_temporary_file_only", + }, + { + "label": "menu entry: a failed write leaves its temporary file", + "file": "beantester/desktop.py", + "old": " os.unlink(temporary)\n", + "new": " pass\n", + "test": "test_a_failed_write_takes_away_its_own_temporary_file_only", + }, + { + "label": "menu entry: the window's class is Tk's shared one", + "file": "beantester/desktop.py", + "old": "WINDOW_CLASS = \"Beannetworktester\"", + "new": "WINDOW_CLASS = \"Tk\"", + "test": "test_the_class_is_written_the_way_tk_gives_it", + }, + { + "label": "menu entry: remove removes nothing", + "file": "beantester/desktop.py", + "old": " os.unlink(target)\n", + "new": " pass\n", + "test": "test_remove_takes_only_its_own_file", + }, + { + "label": "window: Tk's default class, which no menu entry names", + "file": "beantester/cli.py", + "old": " root = tk.Tk(className=desktop.WINDOW_CLASS)\n", + "new": " root = tk.Tk()\n", + "test": "test_the_window_carries_the_class_the_entry_names", + }, + { + "label": "--install-desktop: a refusal answers 0", + "file": "beantester/cli.py", + "old": " _fail(exitcodes.RUNTIME, reason)\n", + "new": " return exitcodes.OK\n", + "test": "test_the_options_answer_with_exit_codes", + }, + { + "label": "--remove-desktop installs", + "file": "beantester/cli.py", + "old": " path, error = desktop.install() if install else desktop.remove()\n", + "new": " path, error = desktop.install()\n", + "test": "test_the_options_answer_with_exit_codes", + }, + { + "label": "package smoke: any window class passes", + "file": "tools/ci_linux_package_smoke.py", + "old": " failures = [] if found and found.group(2) == WINDOW_CLASS else [\n", + "new": " failures = [] if True else [\n", + "test": "test_a_window_that_did_not_open_or_spoke_is_named", + }, + { + "label": "package smoke: an entry for another program passes", + "file": "tools/ci_linux_package_smoke.py", + "old": " if f'Exec=\"{program}\"' not in text.splitlines():\n", + "new": " if False:\n", + "test": "test_every_wrong_menu_entry_is_named", + }, + { + "label": "package smoke: desktop-file-validate is not heard", + "file": "tools/ci_linux_package_smoke.py", + "old": " if validated:\n", + "new": " if False:\n", + "test": "test_every_wrong_menu_entry_is_named", + }, + { + "label": "package smoke: the validator's exit code is not heard", + "file": "tools/ci_linux_package_smoke.py", + "old": " if returncode != 0:\n", + "new": " if False:\n", + "test": "test_the_validator_is_heard_by_its_exit_code_and_both_streams", + }, + { + "label": "package smoke: what the validator says on stderr is not heard", + "file": "tools/ci_linux_package_smoke.py", + "old": " said = (stdout + stderr).strip()\n", + "new": " said = stdout.strip()\n", + "test": "test_the_validator_is_heard_by_its_exit_code_and_both_streams", + }, + { + "label": "package smoke: looks for another class than the window has", + "file": "tools/ci_linux_package_smoke.py", + "old": "WINDOW_CLASS = \"Beannetworktester\"", + "new": "WINDOW_CLASS = \"BeanNetworkTester\"", + "test": "test_it_looks_for_what_the_program_writes", + }, { "label": "pkexec: a DISPLAY sudo kept is replaced", "file": "beantester/linuxenv.py", diff --git a/tests/test_sbom.py b/tests/test_sbom.py index e7bc167f..1b1a1fed 100644 --- a/tests/test_sbom.py +++ b/tests/test_sbom.py @@ -70,21 +70,27 @@ def test_every_registry_component_reaches_the_sbom(): def test_the_declared_licences_are_real_spdx_and_match_the_registry(): """A licence expression that is not a real SPDX id is worse than NOASSERTION: it looks authoritative to a tool that cannot check it.""" - doc = sbom.build() - by_name = {p["name"]: p for p in doc["packages"]} # Hand-curated on purpose: the point is to catch an INVENTED identifier, and a # list fetched at test time would need the network. Each entry was read off the # official SPDX list before being added here - "Unlicense" (full name "The # Unlicense", active, OSI-approved) checked at spdx.org/licenses on 2026-08-17 - # when Tcl 9 brought libtommath into the bundle. + # when Tcl 9 brought libtommath into the bundle; "LGPL-2.1-only" and + # "BSD-2-Clause" (libbpf, the Linux package) on 2026-10-28, neither deprecated. known_ids = { "GPL-3.0-only", "LGPL-3.0-only", "GPL-2.0-only", "LGPL-3.0-or-later", "GPL-2.0-or-later", "BSD-3-Clause", "PSF-2.0", "TCL", "Zlib", "MIT", - "Unlicense", + "Unlicense", "LGPL-2.1-only", "BSD-2-Clause", } known_exceptions = {"Bootloader-exception"} - for name, _module, _prose, _url, expression in legal.COMPONENTS: + for platform in ("win32", "linux"): # both builds, wherever this runs + _check_expressions(sbom.build(platform=platform), legal.components(platform), + known_ids, known_exceptions) + + +def _check_expressions(doc, components, known_ids, known_exceptions): + by_name = {p["name"]: p for p in doc["packages"]} + for name, _module, _prose, _url, expression in components: package = by_name[name] check(f"{name}: the SBOM declares the registry's expression", package["licenseDeclared"] == expression, f"({package['licenseDeclared']})") @@ -134,14 +140,17 @@ def test_the_bundle_guard_notices_a_component_the_registry_lacks(tmp_path): {"name": "Python"}, {"name": "zlib"}, {"name": "WinDivert64"}, {"name": "api-ms-win-core-file-l1-1-0.dll"}, {"name": "Bean Network Tester"}, ]}), encoding="utf-8") - check("a scan of known components is accepted", sbom.audit_bundle(str(clean)) == [], - f"({sbom.audit_bundle(str(clean))})") + check("a scan of known components is accepted", + sbom.audit_bundle(str(clean), "win32") == [], + f"({sbom.audit_bundle(str(clean), 'win32')})") + check("the Windows runtime is not the Linux package's", + any("WinDivert" in line for line in sbom.audit_bundle(str(clean), "linux"))) dirty = tmp_path / "dirty.json" dirty.write_text(json.dumps({"artifacts": [ {"name": "Python"}, {"name": "libcurl"}, {"name": "OpenSSL"}, ]}), encoding="utf-8") - found = sbom.audit_bundle(str(dirty)) + found = sbom.audit_bundle(str(dirty), "win32") check("an unknown component is reported", "libcurl" in found and "OpenSSL" in found, f"({found})") @@ -196,3 +205,22 @@ def missing(name): packages = {p["name"]: p["versionInfo"] for p in sbom.build()["packages"]} check("and the SBOM says NOASSERTION, not a made-up version", packages[named] == "NOASSERTION", f"({packages[named]})") + + +def test_the_linux_package_has_an_sbom_of_its_own(): + """Two archives per release, two documents: each names its own build and has its + own namespace, and every LicenseRef the Linux one uses has its text.""" + windows = sbom.build("v1.2.3", "win32") + linux = sbom.build("v1.2.3", "linux") + check("its own name", linux["name"].endswith("-linux") and not windows["name"].endswith("-linux")) + check("its own namespace", linux["documentNamespace"] != windows["documentNamespace"]) + names = {p["name"] for p in linux["packages"]} + check("eBPFDivert and the X.Org libraries, no WinDivert", + {"eBPFDivert", "X.Org libraries"} <= names and "WinDivert" not in names, f"({names})") + refs = {p["licenseDeclared"] for p in linux["packages"] if "LicenseRef" in p["licenseDeclared"]} + texts = {e["licenseId"] for e in linux.get("hasExtractedLicensingInfo", [])} + check("every LicenseRef has its text", refs <= texts, f"({refs - texts})") + versions = {p["name"]: p["versionInfo"] for p in linux["packages"]} + for name, version in legal.EBPFDIVERT_PARTS.items(): + check(f"{name} inside libebpfdivert.so carries its version", + versions.get(name) == version, f"({versions.get(name)})") diff --git a/tests/test_summary_repro_views.py b/tests/test_summary_repro_views.py index b6d68353..91b69472 100644 --- a/tests/test_summary_repro_views.py +++ b/tests/test_summary_repro_views.py @@ -161,6 +161,43 @@ def test_the_command_on_linux_is_the_one_its_readme_runs(): check("no home known: python3", say("/x/bin/python", True, "") == f"python3 {LAUNCHER}") +def test_the_linux_package_names_its_own_program(): + """The ``.tar.gz`` has no interpreter to start: the command is the program, from + ``~`` when it is in the person's home, else from its own folder - never a path + from the machine. It said ``BeanNetworkTester.exe`` (measured, 2026-10-28).""" + from beantester.appinfo import LINUX_EXE_NAME, linux_command + program = "/u/ana/BeanNetworkTester/BeanNetworkTester" + say = lambda exe, simulate=False, home="/u/ana": linux_command( # noqa: E731 + simulate, exe, "/u/ana/bnt", home, frozen=True) + check("in the home, a capture: sudo and the program from ~", + say(program) == "sudo ~/BeanNetworkTester/BeanNetworkTester", f"({say(program)})") + check("a simulation needs no root", say(program, True) == "~/BeanNetworkTester/BeanNetworkTester") + check("elsewhere: from its own folder, no path", + say("/opt/bnt/BeanNetworkTester") == f"sudo ./{LINUX_EXE_NAME}") + check("a space is quoted, ~ is left for the shell", + say("/u/ana/My Apps/BeanNetworkTester", True) == "~/'My Apps/BeanNetworkTester'") + + +def test_the_frozen_command_follows_the_system_it_runs_on(monkeypatch): + """The exe on Windows; on Linux the program itself, under sudo for a capture - + the repro line, --help's examples and the window's copied command.""" + import sys + + from beantester import appinfo, paths + monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(sys, "platform", "win32") + check("Windows: the exe", appinfo.command_name() == appinfo.EXE_NAME + and appinfo.program_name() == appinfo.EXE_NAME) + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setattr(sys, "executable", "/u/ana/BeanNetworkTester/BeanNetworkTester") + monkeypatch.setattr(appinfo, "_persons_home", lambda: "/u/ana") + check("Linux: the program, under sudo", + appinfo.command_name() == "sudo ~/BeanNetworkTester/BeanNetworkTester", + f"({appinfo.command_name()})") + check("argparse: no .exe", appinfo.program_name() == appinfo.LINUX_EXE_NAME, + f"({appinfo.program_name()})") + + def test_the_command_follows_the_system_it_runs_on(monkeypatch): """Windows from source keeps ``python bean_network_tester.py``; Linux asks ``linux_command``, with root only for a capture.""" diff --git a/tests/test_user_data_location.py b/tests/test_user_data_location.py index 7ef2cd28..decd06a3 100644 --- a/tests/test_user_data_location.py +++ b/tests/test_user_data_location.py @@ -48,6 +48,7 @@ def _frozen_paths(tmp_path, exe_dir, **env): environment = dict(os.environ) environment.pop(paths.DATA_DIR_ENV, None) environment["LOCALAPPDATA"] = str(tmp_path / "localappdata") + environment["XDG_CONFIG_HOME"] = (tmp_path / "xdgconfig").as_posix() environment.update(env) out = subprocess.run([sys.executable, str(script), str(exe_dir), ROOT], capture_output=True, text=True, env=environment, @@ -65,9 +66,66 @@ def test_a_frozen_build_keeps_no_user_file_next_to_the_executable(tmp_path): inside = [name for name in ("profiles", "stats", "connections", "ui") if os.path.dirname(got[name]) == str(exe_dir)] check("no user file is written next to the exe", not inside, f"({inside})") - check("the data directory is under LOCALAPPDATA", - got["data_dir"] == str(tmp_path / "localappdata" / "BeanNetworkTester"), - f"({got['data_dir']})") + if sys.platform.startswith("linux"): + check("the data directory is in the person's config folder", + got["data_dir"] == (tmp_path / "xdgconfig" / "BeanNetworkTester").as_posix(), + f"({got['data_dir']})") + else: + check("the data directory is under LOCALAPPDATA", + got["data_dir"] == str(tmp_path / "localappdata" / "BeanNetworkTester"), + f"({got['data_dir']})") + + +def test_the_linux_package_keeps_the_files_in_the_persons_config_folder(monkeypatch): + """The ``.tar.gz`` (owner, 2026-10-28): ``~/.config/BeanNetworkTester``, not + ``~/AppData/Local`` - which it used, measured - and never beside the program, + which a newer copy unpacked over the old folder would take with it.""" + monkeypatch.delenv(paths.DATA_DIR_ENV, raising=False) + monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(sys, "platform", "linux") + monkeypatch.setattr(paths, "invoking_user", lambda: None) + monkeypatch.setenv("XDG_CONFIG_HOME", "/u/ana/.cfg") + check("XDG_CONFIG_HOME/BeanNetworkTester", + paths.user_data_dir() == "/u/ana/.cfg/BeanNetworkTester", f"({paths.user_data_dir()})") + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setenv("LOCALAPPDATA", "/w/local") + check("Windows keeps LOCALAPPDATA", + paths.user_data_dir() == os.path.join("/w/local", "BeanNetworkTester"), + f"({paths.user_data_dir()})") + + +def test_the_config_folder_is_the_xdg_one_and_the_persons_under_root(): + """``$XDG_CONFIG_HOME`` when it is absolute (the XDG spec ignores a relative + one), else ``~/.config``; root started for a person writes into theirs, not + into root's - sudo's environment carries root's XDG_CONFIG_HOME or none.""" + home = {1000: "/u/ana"}.get + say = lambda env, person=None: paths.config_home(env, person, home) # noqa: E731 + check("absolute XDG_CONFIG_HOME", say({"XDG_CONFIG_HOME": "/u/ana/.cfg", "HOME": "/u/ana"}) + == "/u/ana/.cfg") + check("a relative one is ignored", say({"XDG_CONFIG_HOME": "cfg", "HOME": "/u/ana"}) + == "/u/ana/.config") + check("none: ~/.config", say({"HOME": "/u/ana"}) == "/u/ana/.config") + check("root for a person: theirs, whatever root's says", + say({"XDG_CONFIG_HOME": "/root/.cfg", "HOME": "/root"}, (1000, 1000)) == "/u/ana/.config") + check("a person the system does not know: the environment's", + say({"HOME": "/root"}, (4242, 4242)) == "/root/.config") + + +def test_the_linux_package_adopts_nothing_beside_itself(tmp_path, monkeypatch): + """No Linux build ever kept files beside the program, so there is nothing to + adopt - and the copy under pkexec would make root's copies of whatever lay there.""" + beside, data = tmp_path / "bundle", tmp_path / "data" + beside.mkdir() + (beside / paths.PROFILE_NAME).write_text("{}", encoding="utf-8") + monkeypatch.setattr(paths, "is_frozen", lambda: True) + monkeypatch.setattr(paths, "executable_dir", lambda: str(beside)) + monkeypatch.setattr(paths, "user_data_dir", lambda: str(data)) + monkeypatch.setattr(sys, "platform", "linux") + check("nothing adopted", paths.migrate_user_files() == [] and not data.exists(), + f"({sorted(os.listdir(data)) if data.exists() else []})") + monkeypatch.setattr(sys, "platform", "win32") + paths.migrate_user_files() + check("Windows still adopts", (data / paths.PROFILE_NAME).exists(), "(not copied)") def test_every_user_file_lands_in_the_data_directory(tmp_path): diff --git a/tests/test_windows.py b/tests/test_windows.py index 4c7e317d..98861157 100644 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -411,9 +411,12 @@ def test_about_window_shows_version_author_licence_and_third_parties(): assert panel is not None and panel.is_open() assert app.windows.open_ids() == ["about"] - # the report the window renders names every shipped component + # the report the window renders names every shipped component - this + # system's build: the Linux package carries eBPFDivert, not WinDivert + import sys + divert = "eBPFDivert" if sys.platform.startswith("linux") else "WinDivert" names = [row[0] for row in legal.component_rows()] - assert "WinDivert" in names and "PyDivert" in names and "psutil" in names + assert divert in names and "PyDivert" in names and "psutil" in names # it survives a language switch (the registry rebuilds open windows) app._lang = "en" diff --git a/tools/ci_linux_capture_smoke.py b/tools/ci_linux_capture_smoke.py index 50742ba1..7db338bb 100644 --- a/tools/ci_linux_capture_smoke.py +++ b/tools/ci_linux_capture_smoke.py @@ -55,6 +55,11 @@ Not Linux, not root, or no eBPFDivert -> SKIP and exit 0, unless ``--require`` is given, which CI passes: there a check that cannot run must fail, not pass quietly. + +``--program `` runs the sessions with that program - the frozen Linux +bundle - instead of ``python -m beantester``, and only the two session phases: the +tools' phases call the package's own functions from the source tree, which the +Linux capture job covers already. """ import argparse import json @@ -525,9 +530,20 @@ def open_ping_sockets(): check=True, capture_output=True, text=True) +# The program the sessions run (--program): None is the source tree's. +PROGRAM = None + + +def session_command(args): + """The command line of a session: ``--program``'s executable, else the source tree.""" + if PROGRAM: + return [PROGRAM, *args] + return [sys.executable, "-m", "beantester", *args] + + def run_session(args, during): """The real CLI inside the namespace; ``during()`` runs once the session is up.""" - p = subprocess.Popen(in_namespace(sys.executable, "-m", "beantester", *args), cwd=ROOT, + p = subprocess.Popen(in_namespace(*session_command(args)), cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env={**os.environ, "LANG": "C.UTF-8"}) lines, up = [], threading.Event() @@ -802,7 +818,12 @@ def main(argv=None, environment=None): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--require", action="store_true", help="fail instead of skipping when this machine cannot run the check") + parser.add_argument("--program", metavar="EXECUTABLE", + help="run the sessions with this program (the frozen bundle), " + "and only the session phases") args = parser.parse_args(argv) + global PROGRAM + PROGRAM = os.path.abspath(args.program) if args.program else None if environment is None: try: from pydivert import bpf @@ -821,6 +842,8 @@ def main(argv=None, environment=None): start_far_end() failures += _latency_phase() failures += _loss_phase() + if PROGRAM: + return _report(failures) failures += _ping_phase() failures += _reset_phase() failures += _health_phase() @@ -829,6 +852,10 @@ def main(argv=None, environment=None): failures.append(f"{type(exc).__name__}: {exc}") finally: teardown_namespace() + return _report(failures) + + +def _report(failures): for failure in failures: print("FAIL:", failure) print("OK" if not failures else f"{len(failures)} failure(s)") diff --git a/tools/ci_linux_package_smoke.py b/tools/ci_linux_package_smoke.py new file mode 100644 index 00000000..72630a37 --- /dev/null +++ b/tools/ci_linux_package_smoke.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""The Linux package as a person gets it: the frozen bundle runs, and says the right things. + +Run by CI on the bundle it just built, as an ordinary user under Xvfb (the +capture itself is ``ci_linux_capture_smoke.py --program``, which needs root). +Each phase is a judge over what the program printed, so each can be shown to +fail (``tests/test_ci_linux_package_smoke.py``): + +* ``--version``, a simulated session and a configuration error answer 0, 0 and 3, + as the Windows exe's smoke asks; +* the session's "Reproduce:" line names the program itself - no ``.exe``, no + interpreter, no ``sudo`` for ``--simulate`` (it said ``BeanNetworkTester.exe``); +* ``--doctor`` puts the user files in ``$XDG_CONFIG_HOME/BeanNetworkTester`` + (it said ``~/AppData/Local``); +* the window opens on the display, and says nothing on stderr: a bundled + fontconfig older than the system's printed a warning per rule it rejected; +* the window's class is the one the menu entry names (Tk's default is "Tk", + which GNOME matches to no entry); +* ``--install-desktop`` writes an entry ``desktop-file-validate`` accepts, which + starts this program, and ``--remove-desktop`` takes it away again. + + xvfb-run -a python tools/ci_linux_package_smoke.py dist/BeanNetworkTester/BeanNetworkTester +""" +import os +import re +import subprocess +import sys +import tempfile +import time + +WINDOW_TITLE = "Bean Network Tester" +WINDOW_CLASS = "Beannetworktester" # beantester/desktop.py WINDOW_CLASS (Tk's form) +ENTRY_FILE = "com.donislawdev.BeanNetworkTester.desktop" +WINDOW_WAIT_S = 30 +CONFIG_ERROR = 3 + +# What a clean start must never print (each was seen: tools/linux_bundle.py, cli.py). +NOISE = ("Fontconfig", "Traceback", "symbol lookup error", "No tkinter", "could not open") + + +def judge_codes(version, simulated, bad): + failures = [] + if version != 0: + failures.append(f"--version answered {version}, not 0") + if simulated != 0: + failures.append(f"a simulated session answered {simulated}, not 0") + if bad != CONFIG_ERROR: + failures.append(f"a configuration error answered {bad}, not {CONFIG_ERROR}") + return failures + + +def judge_repro(output): + """The "Reproduce:" line of a ``--simulate`` session.""" + found = re.search(r"Reproduce: (\S+)(.*)", output) + if not found: + return ["the session printed no Reproduce: line"] + program, rest = found.group(1), found.group(2) + failures = [] + if program == "sudo": + failures.append("a simulation is reproduced under sudo") + if not (program.startswith("~/") or program == "./BeanNetworkTester"): + failures.append(f"the program is named {program!r}, not from ~ or its own folder") + if ".exe" in program or "python" in program: + failures.append(f"the program is named {program!r}") + if "--simulate" not in rest: + failures.append("the Reproduce: line lost --simulate") + return failures + + +def judge_doctor(output, config_home): + want = f"user files: {config_home}/BeanNetworkTester" + if want not in output: + line = next((l for l in output.splitlines() if "user files" in l), "no such line") + return [f"--doctor: expected '{want}', got '{line.strip()}'"] + return [] + + +def judge_window(line, stderr): + """``line``: the window's line in ``xwininfo -root -tree``, or None.""" + if line is None: + failures = [f"no window named {WINDOW_TITLE!r} in {WINDOW_WAIT_S} s"] + else: + found = re.search(r'\("([^"]*)" "([^"]*)"\)', line) + failures = [] if found and found.group(2) == WINDOW_CLASS else [ + f"the window's class is not {WINDOW_CLASS!r}: {line.strip()!r}"] + noisy = [l for l in stderr.splitlines() if any(word in l for word in NOISE)] + if noisy: + failures.append(f"the window printed {len(noisy)} line(s) like {noisy[0]!r}") + return failures + + +def judge_entry(installed, text, validated, program, removed): + """``--install-desktop`` and ``--remove-desktop``: codes, the file, the validator.""" + failures = [] + if installed != 0 or text is None: + return [f"--install-desktop answered {installed} and wrote {'nothing' if text is None else 'a file'}"] + if f'Exec="{program}"' not in text.splitlines(): + failures.append("the entry does not start this program") + if f"StartupWMClass={WINDOW_CLASS}" not in text.splitlines(): + failures.append("the entry does not name the window's class") + if validated: + failures.append(f"desktop-file-validate: {validated.strip().splitlines()[0]}") + if not removed: + failures.append("--remove-desktop left the entry") + return failures + + +def validator_says(returncode, stdout, stderr): + """What ``desktop-file-validate`` objected to, or "" when it took the entry as it + is. Its exit code says whether it found an error, which it can report on either + stream or on none; a warning or a hint comes with exit 0 and still counts.""" + said = (stdout + stderr).strip() + if returncode != 0: + return said or f"exit {returncode} without a word" + return said + + +def _run(program, *args, env=None, timeout=60): + done = subprocess.run([program, *args], capture_output=True, text=True, + env=env, timeout=timeout, check=False) + return done.returncode, done.stdout + done.stderr + + +def window_line(title=WINDOW_TITLE): + """The line ``xwininfo -root -tree`` gives the top-level window of that title + (it carries the window's class), or None.""" + tree = subprocess.run(["xwininfo", "-root", "-tree"], capture_output=True, + text=True, timeout=10, check=False).stdout + return next((l for l in tree.splitlines() if f'"{title}": (' in l), None) + + +def _window_phase(program, env): + started = subprocess.Popen([program], env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, text=True) + seen = None + try: + deadline = time.monotonic() + WINDOW_WAIT_S + while time.monotonic() < deadline and started.poll() is None: + seen = window_line() + if seen: + time.sleep(2) # what a start prints comes in its first seconds + break + time.sleep(0.5) + finally: + started.terminate() + try: + _out, err = started.communicate(timeout=20) + except subprocess.TimeoutExpired: + started.kill() + _out, err = started.communicate() + return judge_window(seen, err or "") + + +def run_phases(program, scratch): + env = {**os.environ, "LANG": "C.UTF-8", "BEAN_NO_ELEVATE": "1", + "BEAN_DATA_DIR": os.path.join(scratch, "data")} + version, _ = _run(program, "--version", env=env) + simulated, output = _run(program, "--simulate", "--duration", "2", "--interval", "1", + "--seed", "42", env=env) + bad, _ = _run(program, "--simulate", "--loss", "250", env=env) + failures = judge_codes(version, simulated, bad) + failures += judge_repro(output) + config_home = os.path.join(scratch, "config") + doctor_env = {k: v for k, v in env.items() if k != "BEAN_DATA_DIR"} + doctor_env["XDG_CONFIG_HOME"] = config_home + _code, report = _run(program, "--doctor", env=doctor_env) + failures += judge_doctor(report, config_home) + failures += _window_phase(program, env) + failures += _entry_phase(program, env, scratch) + return failures + + +def _entry_phase(program, env, scratch): + env = {**env, "XDG_DATA_HOME": os.path.join(scratch, "share")} + entry = os.path.join(scratch, "share", "applications", ENTRY_FILE) + installed, _ = _run(program, "--install-desktop", env=env) + text = None + if os.path.isfile(entry): + with open(entry, encoding="utf-8") as handle: + text = handle.read() + validated = "" + if text: + done = subprocess.run(["desktop-file-validate", entry], capture_output=True, + text=True, timeout=30, check=False) + validated = validator_says(done.returncode, done.stdout, done.stderr) + _run(program, "--remove-desktop", env=env) + return judge_entry(installed, text, validated, program, not os.path.exists(entry)) + + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + if len(argv) != 1: + print("usage: ci_linux_package_smoke.py ", file=sys.stderr) + return 2 + if not sys.platform.startswith("linux") or not os.environ.get("DISPLAY"): + print("FAIL: needs Linux and a display (run it under xvfb-run)") + return 1 + with tempfile.TemporaryDirectory(prefix="bnt-package-") as scratch: + failures = run_phases(os.path.abspath(argv[0]), scratch) + for failure in failures: + print("FAIL:", failure) + print("OK" if not failures else f"{len(failures)} failure(s)") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/linux_bundle.py b/tools/linux_bundle.py new file mode 100644 index 00000000..a9718413 --- /dev/null +++ b/tools/linux_bundle.py @@ -0,0 +1,97 @@ +"""What the Linux bundle takes from the system instead of carrying - and the check +that a built bundle keeps to it. + +``BeanNetworkTester.spec`` drops ``FROM_SYSTEM`` from what PyInstaller collects on +Linux, and CI runs ``check`` on the folder it built, so the list is in one place +and a bundle that drifts from it fails the build instead of a person's desktop. + +Why these, MEASURED 2026-10-28 with a bundle built on Ubuntu 24.04 and run on +Ubuntu 26.04, Fedora 44 and Arch: + +* A bundled fontconfig reads the SYSTEM's configuration files, and an older one + rejects a newer system's: on Arch (fontconfig 2.18) the window printed 15 + "Fontconfig warning: ... invalid attribute 'xsi:nil'" lines and dropped those + rules. freetype goes with it (a system fontconfig needs its own), and so do what + the two read with: expat, libpng, brotli and bzip2 (PyInstaller collects + freetype's libraries before the spec drops freetype - measured: libbz2 stayed). + Every desktop has all of them. +* libXft stays: without it in the bundle, a system that does not have it (Fedora + 44 and Arch as installed, both measured) opened no window at all. +* libXau and libXdmcp: the system's libxcb - which PyInstaller never bundles - + needs both (ldd on Ubuntu 26.04 and Arch), so every system that can show a + window has them. libbsd and libmd came only with Ubuntu 24.04's libXdmcp. + +``NOT_NEEDED_MODULES`` are standard modules the program never uses - no bz2, lzma +or zstd anywhere in it - which the standard library only imports when it can +(``shutil``, ``zipfile``). Left out on Linux, with the libraries they would bring - +and BLT, a Tk extension Ubuntu's python3-tk installs and PyInstaller then collected +(measured in an ubuntu:24.04 image); nothing here loads it. + +``MUST_CARRY`` are the parts a bundle without them would ship broken: Tk and Xft +for the window, and pydivert's ``libebpfdivert.so`` for any capture. + + python tools/linux_bundle.py check dist/BeanNetworkTester +""" +import os +import sys + +FROM_SYSTEM = ("libfontconfig.so", "libfreetype.so", "libexpat.so", "libpng16.so", + "libbrotlicommon.so", "libbrotlidec.so", "libbz2.so", + "libXau.so", "libXdmcp.so", "libbsd.so", "libmd.so") + +NOT_NEEDED_MODULES = ("_bz2", "_lzma", "_zstd") +NOT_NEEDED_LIBRARIES = ("liblzma.so", "libzstd.so", "libBLT") + +MUST_CARRY = ("libtk", "libXft.so", "libebpfdivert.so") + + +def taken_from_system(name): + """True for a library file the bundle must not carry (by its file name).""" + return os.path.basename(name).startswith(FROM_SYSTEM) + + +def not_needed(name): + """True for a module or library the program never uses (by its file name).""" + base = os.path.basename(name) + return (base.startswith(NOT_NEEDED_LIBRARIES) + or any(base.startswith(module + ".") for module in NOT_NEEDED_MODULES)) + + +def libraries(folder): + """The file names of every shared library under ``folder``.""" + found = [] + for _base, _dirs, files in os.walk(folder): + found.extend(name for name in files if ".so" in name) + return sorted(found) + + +def problems(folder): + """What is wrong with the bundle in ``folder``: an empty list when nothing is.""" + if not os.path.isdir(folder): + return [f"{folder}: no such folder"] + names = libraries(folder) + wrong = [f"carries {name}, which must come from the system" + for name in names if taken_from_system(name)] + wrong += [f"carries {name}, which the program never uses" + for name in names if not_needed(name)] + missing = [f"does not carry {part}" for part in MUST_CARRY + if not any(name.startswith(part) for name in names)] + return wrong + missing + + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + if len(argv) != 2 or argv[0] != "check": + print("usage: linux_bundle.py check ", file=sys.stderr) + return 2 + found = problems(argv[1]) + for line in found: + print(f"linux bundle: {line}", file=sys.stderr) + if not found: + print(f"linux bundle: {len(libraries(argv[1]))} libraries, none of them the " + f"system's own, Tk, Xft and libebpfdivert present") + return 1 if found else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/sbom.py b/tools/sbom.py index cf094069..6e198e04 100644 --- a/tools/sbom.py +++ b/tools/sbom.py @@ -28,6 +28,9 @@ python tools/sbom.py # SPDX JSON to stdout python tools/sbom.py -o sbom.spdx.json # ...or to a file python tools/sbom.py --audit-bundle dist/BeanNetworkTester # guard mode + +Each describes the build for the system it runs on - the Windows zip on Windows, +the Linux .tar.gz on Linux (``legal.components``) - or ``--platform``'s. """ import argparse import datetime @@ -54,6 +57,13 @@ "under Microsoft's redistributable-code terms for Visual Studio. " "Microsoft publishes the terms rather than a licence text to bundle: " "https://learn.microsoft.com/cpp/windows/redistributing-visual-cpp-files", + # Five libraries, five permission notices of the MIT and X11 kind, each with its + # own wording and holders: one reference with the texts beside it, rather than + # an SPDX expression nobody read the files to choose. + "LicenseRef-X.Org-libraries": + "libX11, libXext, libXft, libXrender and libXss as Ubuntu 24.04 ships them, " + "under the MIT and X11-style permission notices in their copyright files; " + "the Linux package carries the full texts in licenses/X.Org-libraries.txt.", } @@ -78,8 +88,10 @@ def _package(name, version, url, spdx_expression): } -def build(namespace_seed=None): +def build(namespace_seed=None, platform=None): """The SBOM as a dict, ready for ``json.dump``.""" + platform = sys.platform if platform is None else platform + linux = platform.startswith("linux") version = appinfo.__version__ root_id = _spdx_id("Package", appinfo.APP_NAME) created = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") @@ -88,6 +100,8 @@ def build(namespace_seed=None): # caller has something stable to offer (a tag), so two runs on the same tag # produce the same URI instead of a random one nobody can correlate. seed = namespace_seed or created + if linux: # two documents per release: each its own namespace + seed += "|linux" digest = hashlib.sha256(("%s|%s" % (version, seed)).encode("utf-8")).hexdigest()[:16] packages = [{ @@ -106,8 +120,9 @@ def build(namespace_seed=None): "relatedSpdxElement": root_id, }] - rows = {name: version for name, version, _lic, _url in legal.component_rows()} - for name, _module, _prose, url, spdx_expression in legal.COMPONENTS: + rows = {name: version for name, version, _lic, _url in legal.component_rows(platform)} + shipped = legal.components(platform) + for name, _module, _prose, url, spdx_expression in shipped: reported = rows.get(name, "") packages.append(_package( name, @@ -119,12 +134,12 @@ def build(namespace_seed=None): "relatedSpdxElement": _spdx_id("Package", name), }) - used = {c[4] for c in legal.COMPONENTS} + used = {c[4] for c in shipped} document = { "spdxVersion": SPDX_VERSION, "dataLicense": DATA_LICENSE, "SPDXID": "SPDXRef-DOCUMENT", - "name": "%s-%s" % (appinfo.APP_NAME, version), + "name": "%s-%s%s" % (appinfo.APP_NAME, version, "-linux" if linux else ""), "documentNamespace": "https://github.com/donislawdev/BeanNetworkTester/spdx/%s-%s" % (version, digest), "creationInfo": { @@ -164,17 +179,23 @@ def build(namespace_seed=None): # an anchored one - `re.search`, so the path prefix does not matter. (r"libtommath", "libtommath"), (r"^pydivert", "PyDivert"), + # The Linux .tar.gz. + (r"ebpfdivert", "eBPFDivert"), + (r"libx11|libxext|libxft|libxrender|libxss|x\.org", "X.Org libraries"), + (r"libbpf", "libbpf"), + (r"elfutils|libelf", "elfutils (libelf)"), + (r"zstd|zstandard", "Zstandard"), (r"^psutil", "psutil"), (r"pyinstaller", "PyInstaller (bootloader)"), (r"^bean network tester$|^beannetworktester", None), # the program itself ) -def audit_bundle(scan_json): +def audit_bundle(scan_json, platform=None): """Names in a Syft scan that no registry entry accounts for.""" with open(scan_json, encoding="utf-8") as handle: scan = json.load(handle) - known = {name for name, *_ in legal.COMPONENTS} + known = {name for name, *_ in legal.components(platform)} unaccounted = [] for artifact in scan.get("artifacts", []): name = str(artifact.get("name", "")).strip() @@ -197,10 +218,12 @@ def main(): parser.add_argument("--audit-bundle", metavar="SYFT_JSON", help="guard mode: fail when a Syft scan names something " "the registry does not account for") + parser.add_argument("--platform", choices=("win32", "linux"), + help="the build to describe (default: this system's)") args = parser.parse_args() if args.audit_bundle: - missing = audit_bundle(args.audit_bundle) + missing = audit_bundle(args.audit_bundle, args.platform) if missing: print("The built bundle contains components the licence registry does " "not account for.\nAdd them to beantester/legal.py, " @@ -211,11 +234,12 @@ def main(): print("bundle audit: every component maps to a registry entry") return 0 - text = json.dumps(build(args.namespace_seed), indent=2, sort_keys=False) + "\n" + document = build(args.namespace_seed, args.platform) + text = json.dumps(document, indent=2, sort_keys=False) + "\n" if args.output: with open(args.output, "w", encoding="utf-8") as handle: handle.write(text) - print("wrote %s (%d packages)" % (args.output, len(build()["packages"]))) + print("wrote %s (%d packages)" % (args.output, len(document["packages"]))) else: sys.stdout.write(text) return 0