426 lines
24 KiB
Python
426 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Inventory a trusted DocView install and its Arch Linux system dependencies.
|
|
|
|
This does not download sources, infer a license choice, or certify redistribution.
|
|
Only use ldd on executables built by this project, never on an input document.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from urllib.parse import quote
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EXECUTABLES = ('docview', 'docview-pdf-worker', 'docview-archive-worker')
|
|
MAX_FILES = 10000
|
|
MAX_COMPONENTS = 256
|
|
MAX_LICENSE_BYTES = 32 * 1024 * 1024
|
|
QT_EMBEDDED_NOTICE_METADATA_SHA256 = '8d90e93aa487eddef4b81722a53b89953cbb07b45d02488e8ea0413f300fd723'
|
|
|
|
|
|
def sha256(path):
|
|
with Path(path).open('rb') as stream:
|
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
|
|
|
|
def run(args):
|
|
environment = {**os.environ, 'LC_ALL': 'C'}
|
|
for key in ('LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT'):
|
|
environment.pop(key, None)
|
|
result = subprocess.run(args, check=True, text=True, capture_output=True, timeout=30,
|
|
env=environment)
|
|
return result.stdout
|
|
|
|
|
|
def pacman_fields(text):
|
|
result, key = {}, None
|
|
for line in text.splitlines():
|
|
if line.startswith('%') and line.endswith('%'):
|
|
key = line[1:-1]
|
|
result[key] = []
|
|
elif line and key:
|
|
result[key].append(line)
|
|
return result
|
|
|
|
|
|
class PackageDatabase:
|
|
def __init__(self, root=Path('/var/lib/pacman/local')):
|
|
self.packages, self.owners = {}, {}
|
|
for directory in sorted(root.iterdir()):
|
|
if not directory.is_dir() or not (directory / 'desc').is_file():
|
|
continue
|
|
desc = pacman_fields((directory / 'desc').read_text())
|
|
name = desc['NAME'][0]
|
|
self.packages[name] = desc
|
|
for item in pacman_fields((directory / 'files').read_text()).get('FILES', []):
|
|
if not item.endswith('/'):
|
|
self.owners['/' + item] = name
|
|
|
|
def owner(self, path):
|
|
value = self.owners.get(str(path)) or self.owners.get(str(path.resolve()))
|
|
if not value:
|
|
raise ValueError(f'No installed Arch package owns runtime file: {path}')
|
|
return value
|
|
|
|
|
|
def ldd_paths(output):
|
|
paths = set()
|
|
for line in output.splitlines():
|
|
if 'not found' in line:
|
|
raise ValueError('Unresolved ELF dependency: ' + line.strip())
|
|
value = line.strip()
|
|
if not value or value.startswith('linux-vdso'):
|
|
continue
|
|
match = re.fullmatch(r'(?:\S+ => )?(/.+?) \(0x[0-9a-fA-F]+\)', value)
|
|
if not match:
|
|
raise ValueError('Unrecognized ldd dependency: ' + value)
|
|
paths.add(Path(match[1]))
|
|
return paths
|
|
|
|
|
|
def elf(path):
|
|
with path.open('rb') as stream:
|
|
return stream.read(4) == b'\x7fELF'
|
|
|
|
|
|
def qt_runtime_files(query):
|
|
"""Finite candidate modules; not a claim that every style/plugin was loaded."""
|
|
files = {Path(query['QT_INSTALL_LIBEXECS']) / 'QtWebEngineProcess'}
|
|
qml = Path(query['QT_INSTALL_QML'])
|
|
for relative in ('QtQml', 'QtQuick/Controls', 'QtQuick/Dialogs', 'QtQuick/Layouts',
|
|
'QtQuick/Templates', 'QtQuick/Window', 'QtQuick/NativeStyle',
|
|
'QtQuick/Effects', 'QtWebEngine'):
|
|
directory = qml / relative
|
|
if directory.is_dir():
|
|
files.update(p for p in directory.rglob('*') if p.is_file())
|
|
files.update(p for p in (qml / 'QtQuick').glob('*') if p.is_file())
|
|
plugins = Path(query['QT_INSTALL_PLUGINS'])
|
|
for relative in ('platforms/libqxcb.so', 'platforms/libqwayland-generic.so',
|
|
'platforms/libqwayland-egl.so'):
|
|
path = plugins / relative
|
|
if path.is_file():
|
|
files.add(path)
|
|
for filename in ('libqjpeg.so', 'libqgif.so', 'libqico.so', 'libqsvg.so', 'libqwebp.so'):
|
|
path = plugins / 'imageformats' / filename
|
|
if path.is_file():
|
|
files.add(path)
|
|
for relative in ('xcbglintegrations', 'wayland-graphics-integration-client'):
|
|
directory = plugins / relative
|
|
if directory.is_dir():
|
|
files.update(p for p in directory.rglob('*') if p.is_file())
|
|
resources = Path(query['QT_INSTALL_DATA']) / 'resources'
|
|
files.update(p for p in resources.glob('*') if p.is_file() and
|
|
(p.name.startswith('qtwebengine') or p.name in ('icudtl.dat', 'v8_context_snapshot.bin')))
|
|
translations = Path(query['QT_INSTALL_TRANSLATIONS'])
|
|
for name in ('qtbase_ja.qm', 'qtdeclarative_ja.qm'):
|
|
path = translations / name
|
|
if not path.is_file():
|
|
raise ValueError('Required Japanese Qt translation is missing: ' + name)
|
|
files.add(path)
|
|
files.update(p for p in (translations / 'qtwebengine_locales').glob('*') if p.is_file())
|
|
for pattern in ('qtbase_*.qm', 'qtdeclarative_*.qm', 'qtwebengine_*.qm'):
|
|
files.update(p for p in translations.glob(pattern) if p.is_file())
|
|
if not files or len(files) > MAX_FILES or not all(p.is_file() for p in files):
|
|
raise ValueError('Missing or oversized Qt runtime inventory')
|
|
return files
|
|
|
|
|
|
def license_candidates(name, desc, base=Path('/usr/share/licenses')):
|
|
result = set()
|
|
directory = base / name
|
|
if directory.is_dir():
|
|
result.update(p for p in directory.rglob('*') if p.is_file())
|
|
# Include the generic text when Arch uses a shared SPDX license directory.
|
|
for label in desc.get('LICENSE', []):
|
|
for token in re.findall(r'[A-Za-z0-9][A-Za-z0-9.+-]*', label):
|
|
path = base / 'spdx' / (token + '.txt')
|
|
if path.is_file():
|
|
result.add(path)
|
|
return sorted(result)
|
|
|
|
|
|
def license_supplements(name, version, base=ROOT / 'resources/licenses/linux-supplemental'):
|
|
result = []
|
|
upstream_version = version.rsplit('-', 1)[0].split(':')[-1]
|
|
for row in json.loads((base / 'sources.json').read_text()):
|
|
if row['package'] != name:
|
|
continue
|
|
if row['version'] != upstream_version:
|
|
raise ValueError('Supplemental license version needs review: ' + name)
|
|
for filename, expected in sorted(row['files'].items()):
|
|
if Path(filename).name != filename:
|
|
raise ValueError('Unsafe supplemental license filename')
|
|
path = base / name / filename
|
|
if sha256(path) != expected:
|
|
raise ValueError('Supplemental license digest mismatch: ' + str(path))
|
|
result.append((path, row))
|
|
return result
|
|
|
|
|
|
def qt_embedded_notices(qt_version, package_version, system_files,
|
|
base=ROOT / 'resources/licenses/qt-embedded-notices'):
|
|
"""Bind the reviewed partial notice set to the exact system DataPack variant."""
|
|
source = base / 'sources.json'
|
|
if not source.is_file() or source.stat().st_size > 65536:
|
|
raise ValueError('Qt embedded notice metadata missing or oversized')
|
|
metadata_bytes = source.read_bytes()
|
|
if hashlib.sha256(metadata_bytes).hexdigest() != QT_EMBEDDED_NOTICE_METADATA_SHA256:
|
|
raise ValueError('Qt embedded notice metadata changed; review required')
|
|
value = json.loads(metadata_bytes)
|
|
if (value['schemaVersion'] != 1 or value['qtVersion'] != qt_version or
|
|
value['packageVersion'] != package_version or value['package'] != 'qt6-webengine' or
|
|
value['completeChromiumNotices'] is not False or value['runtimeDistribution'] != 'system-not-bundled'):
|
|
raise ValueError('Qt embedded notice target version or scope needs review')
|
|
for expected in value['dataPacks']:
|
|
matches = [row for row in system_files if row['path'] == expected['path']]
|
|
if (len(matches) != 1 or any(matches[0].get(key) != expected[key] for key in ('package', 'size', 'sha256')) or
|
|
expected['packageVersion'] != package_version):
|
|
raise ValueError('Qt embedded notice DataPack inventory mismatch; review required')
|
|
for row in value['files']:
|
|
if Path(row['name']).name != row['name']:
|
|
raise ValueError('Unsafe Qt embedded notice filename')
|
|
path = base / row['name']
|
|
if (not path.is_file() or path.stat().st_size != row['size'] or sha256(path) != row['sha256']):
|
|
raise ValueError('Qt embedded notice text missing or digest/size mismatch')
|
|
return source, value
|
|
|
|
|
|
def pdfium_supplemental_notices(prefix, locked):
|
|
relative = Path('share/doc/docview/pdfium/supplemental')
|
|
source = prefix / relative / 'sources.json'
|
|
if not source.is_file() or source.stat().st_size > 65536:
|
|
raise ValueError('PDFium supplemental notice metadata missing or oversized')
|
|
value = json.loads(source.read_text())
|
|
if (value.get('schemaVersion') != 1 or value.get('pdfiumVersion') != locked['version'] or
|
|
value.get('pdfiumUpstreamCommit') != locked['upstreamCommit'] or
|
|
value.get('pdfiumLibrarySha256') != sha256(prefix / 'lib/libpdfium.so')):
|
|
raise ValueError('PDFium supplemental notice source or binary needs review')
|
|
rows = value.get('files')
|
|
if (not isinstance(rows, list) or len(rows) != 2 or
|
|
any(not isinstance(r, dict) for r in rows) or {r.get('name') for r in rows} != {
|
|
'libcxx-LICENSE.txt', 'libcxxabi-LICENSE.txt'}):
|
|
raise ValueError('Unexpected PDFium supplemental notice set')
|
|
files = []
|
|
for row in rows:
|
|
path = prefix / relative / row['name']
|
|
if (not path.is_file() or path.stat().st_size > 1024 * 1024 or
|
|
path.stat().st_size != row['size'] or sha256(path) != row['sha256']):
|
|
raise ValueError('PDFium supplemental notice digest or size mismatch')
|
|
files.append({**row, 'path': (relative / row['name']).as_posix()})
|
|
return {'metadataPath': (relative / 'sources.json').as_posix(), 'metadataSha256': sha256(source),
|
|
'scope': value['scope'], 'providerRecipeCommit': value['providerRecipeCommit'],
|
|
'pdfiumLibrarySha256': value['pdfiumLibrarySha256'], 'files': files}
|
|
|
|
|
|
def inventory(prefix, output, qtpaths='/usr/lib/qt6/bin/qtpaths'):
|
|
prefix, output = prefix.resolve(strict=True), output.resolve()
|
|
if sys.platform != 'linux' or platform.machine() != 'x86_64':
|
|
raise ValueError('This inventory targets Arch Linux x86_64 only')
|
|
os_release = platform.freedesktop_os_release()
|
|
if os_release.get('ID') != 'arch':
|
|
raise ValueError('An Arch package database is required; this is not an Ubuntu package')
|
|
if output.exists() and any(output.iterdir()):
|
|
raise ValueError('Inventory output directory must be empty')
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
query = dict(line.split(':', 1) for line in run([qtpaths, '--query']).splitlines() if ':' in line)
|
|
if query.get('QT_VERSION') != '6.11.2':
|
|
raise ValueError('Expected the verified Qt 6.11.2 runtime')
|
|
database = PackageDatabase()
|
|
inputs = [prefix / 'bin' / name for name in EXECUTABLES]
|
|
before = {str(p.relative_to(prefix)): sha256(p) for p in inputs}
|
|
bundled = prefix / 'lib/libpdfium.so'
|
|
expected_pdfium = ROOT / '.deps/pdfium/lib/libpdfium.so'
|
|
if not bundled.is_file() or sha256(bundled) != sha256(expected_pdfium):
|
|
raise ValueError('Installed PDFium differs from the pinned local archive')
|
|
candidates = qt_runtime_files(query)
|
|
system = {p.resolve(): {'qt-runtime-candidate'} for p in candidates}
|
|
edges = []
|
|
for path in inputs + sorted(candidates):
|
|
if not elf(path):
|
|
continue
|
|
label = str(path.relative_to(prefix)) if path.is_relative_to(prefix) else str(path)
|
|
resolved = []
|
|
for dependency in sorted(ldd_paths(run(['ldd', str(path)]))):
|
|
real = dependency.resolve(strict=True)
|
|
if real.is_relative_to(prefix):
|
|
if real != bundled:
|
|
raise ValueError('Unexpected bundled shared library: ' + str(real))
|
|
resolved.append('bundle:lib/libpdfium.so')
|
|
else:
|
|
system.setdefault(real, set()).add('elf-dependency')
|
|
resolved.append(str(real))
|
|
edges.append({'elf': label, 'resolved': sorted(resolved)})
|
|
if len(system) > MAX_FILES:
|
|
raise ValueError('Runtime inventory exceeds file limit')
|
|
components, system_files = {}, []
|
|
# toml++ is compiled into DocView as well as potentially dynamically linked.
|
|
names = {'tomlplusplus', 'qt6-base', 'qt6-declarative', 'qt6-webengine'}
|
|
for path, roles in sorted(system.items()):
|
|
owner = database.owner(path)
|
|
names.add(owner)
|
|
system_files.append({'path': str(path), 'package': owner, 'size': path.stat().st_size,
|
|
'sha256': sha256(path), 'roles': sorted(roles)})
|
|
if len(names) > MAX_COMPONENTS:
|
|
raise ValueError('Runtime inventory exceeds package limit')
|
|
total_license_bytes = 0
|
|
for name in sorted(names):
|
|
desc = database.packages[name]
|
|
version = desc['VERSION'][0]
|
|
base = desc.get('BASE', [name])[0]
|
|
licenses = []
|
|
embedded_notices = None
|
|
for source in license_candidates(name, desc):
|
|
if source.stat().st_size > 8 * 1024 * 1024:
|
|
raise ValueError('License file exceeds limit: ' + str(source))
|
|
total_license_bytes += source.stat().st_size
|
|
if total_license_bytes > MAX_LICENSE_BYTES:
|
|
raise ValueError('License collection exceeds limit')
|
|
relative = Path('licenses') / name / source.relative_to('/usr/share/licenses')
|
|
target = output / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(source, target)
|
|
licenses.append({'path': relative.as_posix(), 'sourcePath': str(source),
|
|
'sha256': sha256(target), 'size': target.stat().st_size})
|
|
for source, provenance in license_supplements(name, version):
|
|
total_license_bytes += source.stat().st_size
|
|
if total_license_bytes > MAX_LICENSE_BYTES:
|
|
raise ValueError('License collection exceeds limit')
|
|
relative = Path('licenses') / name / 'upstream' / source.name
|
|
target = output / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(source, target)
|
|
licenses.append({'path': relative.as_posix(), 'sourceUrl': provenance['url'],
|
|
'upstreamVersion': provenance['version'],
|
|
'downloadSha256': provenance['downloadSha256'],
|
|
'collectionScope': 'Upstream notice text only; not a complete source collection',
|
|
'sha256': sha256(target), 'size': target.stat().st_size})
|
|
if name == 'qt6-webengine':
|
|
metadata_path, reviewed = qt_embedded_notices(query['QT_VERSION'], version, system_files)
|
|
folder = Path('licenses') / name / 'embedded-resource-notices'
|
|
metadata_relative = folder / 'sources.json'
|
|
(output / folder).mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(metadata_path, output / metadata_relative)
|
|
if sha256(output / metadata_relative) != QT_EMBEDDED_NOTICE_METADATA_SHA256:
|
|
raise ValueError('Qt embedded notice metadata changed while copying')
|
|
total_license_bytes += (output / metadata_relative).stat().st_size
|
|
for row in reviewed['files']:
|
|
relative = folder / row['name']
|
|
target = output / relative
|
|
shutil.copyfile(metadata_path.parent / row['name'], target)
|
|
if target.stat().st_size != row['size'] or sha256(target) != row['sha256']:
|
|
raise ValueError('Qt embedded notice text changed while copying')
|
|
total_license_bytes += row['size']
|
|
licenses.append({'path': relative.as_posix(), 'size': row['size'], 'sha256': row['sha256'],
|
|
'origin': 'reviewed-installed-DataPack-resource-comment',
|
|
'sourceResources': row['sourceResources'],
|
|
'collectionScope': reviewed['scope']})
|
|
if total_license_bytes > MAX_LICENSE_BYTES:
|
|
raise ValueError('License collection exceeds limit')
|
|
embedded_notices = {'status': 'collected-reviewed-resource-subset',
|
|
'metadataPath': metadata_relative.as_posix(),
|
|
'metadataSha256': QT_EMBEDDED_NOTICE_METADATA_SHA256,
|
|
'noticeCount': len(reviewed['files']),
|
|
'sourceResourceCount': sum(len(row['sourceResources']) for row in reviewed['files']),
|
|
'runtimeDistribution': reviewed['runtimeDistribution'],
|
|
'completeChromiumNotices': False, 'scope': reviewed['scope']}
|
|
components[name] = {
|
|
'version': version, 'architecture': desc.get('ARCH', ['unknown'])[0],
|
|
'distribution': 'system-not-bundled',
|
|
'upstreamHome': desc.get('URL', [''])[0],
|
|
'licenseLabelsFromPackage': desc.get('LICENSE', []),
|
|
'licenseTexts': licenses,
|
|
'noticeStatus': 'collected-package-or-upstream-texts-not-a-compliance-verdict' if licenses else 'text-not-found',
|
|
'source': {'status': 'not-collected', 'packageBase': base, 'packageVersion': version,
|
|
'recipeRepository': 'https://gitlab.archlinux.org/archlinux/packaging/packages/' + quote(base),
|
|
'recipeCommit': None,
|
|
'note': 'This package contains source pointers, not complete corresponding source. Selected exact recipes and patches are recorded separately in the repository source-correspondence evidence.'}}
|
|
if name == 'tomlplusplus':
|
|
components[name]['usage'] = ['system runtime library', 'headers compiled into DocView']
|
|
if embedded_notices:
|
|
components[name]['embeddedResourceNotices'] = embedded_notices
|
|
locked = json.loads((ROOT / 'cmake/pdfium.lock.json').read_text())
|
|
pdfium_supplements = pdfium_supplemental_notices(prefix, locked)
|
|
pdfium_licenses = []
|
|
for path in sorted((prefix / 'share/doc/docview/pdfium').rglob('*')):
|
|
if path.is_file() and path.name != 'sources.json':
|
|
pdfium_licenses.append({'path': path.relative_to(prefix).as_posix(), 'sha256': sha256(path),
|
|
'size': path.stat().st_size})
|
|
if not pdfium_licenses:
|
|
raise ValueError('PDFium archive license texts missing from install')
|
|
components['PDFium-independent-worker'] = {
|
|
'version': locked['version'], 'distribution': 'bundled', 'path': 'lib/libpdfium.so',
|
|
'sha256': sha256(bundled), 'size': bundled.stat().st_size,
|
|
'licenseTexts': pdfium_licenses, 'upstreamHome': locked['upstream'],
|
|
'supplementalNotices': pdfium_supplements,
|
|
'source': {'status': 'not-collected', 'upstreamCommit': locked['upstreamCommit'],
|
|
'sourceUrl': locked['upstream'], 'binaryProvider': locked['binaryProvider'],
|
|
'binaryArchive': locked['linuxX64Archive'], 'archiveSha256': locked['linuxX64Sha256'],
|
|
'buildOptions': locked['buildOptions'],
|
|
'note': 'Upstream revision and provider archive are pinned; corresponding source tree and all dependency sources are not included.'}}
|
|
versions = {}
|
|
for path in inputs + [bundled]:
|
|
values = sorted(set(re.findall(r'Name: ((?:GLIBC|GLIBCXX|CXXABI)_[A-Za-z0-9_.]+)',
|
|
run(['readelf', '--version-info', str(path)]))))
|
|
versions[path.relative_to(prefix).as_posix()] = values
|
|
if before != {str(p.relative_to(prefix)): sha256(p) for p in inputs}:
|
|
raise ValueError('Installed executables changed during inventory')
|
|
result = {'schemaVersion': 1,
|
|
'scope': 'Local Arch Linux x86_64 development package; not a clean-OS or redistribution acceptance',
|
|
'host': {'id': os_release['ID'], 'prettyName': os_release.get('PRETTY_NAME', ''),
|
|
'architecture': platform.machine(), 'qtVersion': query['QT_VERSION'],
|
|
'glibcPackage': database.packages['glibc']['VERSION'][0]},
|
|
'executableSha256': before, 'requiredSymbolVersions': versions,
|
|
'components': components, 'systemFiles': system_files, 'elfResolution': edges,
|
|
'systemRuntimeCandidateBytes': sum(row['size'] for row in system_files),
|
|
'scopeLimits': [
|
|
'Qt libraries, selected QML/style/plugin candidates, WebEngine helper/resources/locales and ELF closure are system dependencies, not copied runtime binaries.',
|
|
'Candidate styles/plugins are a bounded inventory, not a trace proving every file was used. Other platform, IME, theme, GPU and font providers may load additional files.',
|
|
'System fonts and their licenses are not bundled; FontBroker uses locally installed fonts.',
|
|
'Arch package license labels can list alternatives. This record does not choose a license for DocView or resolve every component condition.',
|
|
'Qt WebEngine includes Chromium third parties. Seven reviewed notice texts from 13 system DataPack resources are included, but neither these nor the top-level package license are a complete build-specific Chromium attribution collection.',
|
|
'Complete corresponding dependency sources are not included in this package. Selected source-correspondence evidence is maintained separately; URLs and license texts alone do not establish source fulfillment.',
|
|
'Ubuntu 24.04, Windows, clean installation and signed distribution remain unverified.']}
|
|
(output / 'dependency-manifest.json').write_text(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + '\n')
|
|
lines = ['DocView Linux development package: third-party notices', '',
|
|
'Local Arch Linux x86_64 artifact. Not a complete redistribution approval.',
|
|
'DocView public license: unspecified. This inventory does not license DocView.',
|
|
'Only the independent PDFium binary is bundled; the other runtime libraries use the host system.',
|
|
'License texts below are copied verbatim from installed packages, recorded versioned upstream sources, or reviewed system DataPack resource comments. Package labels may be alternatives.',
|
|
'Seven notice texts extracted from 13 fixed QtWebEngine DataPack resources are included; the Qt runtime remains system-only and complete Chromium attribution remains uncollected.',
|
|
'Complete corresponding dependency sources and build materials are NOT included in this package.',
|
|
'Qt WebEngine Chromium build-specific notices remain to be completed before a release.',
|
|
'See dependency-manifest.json for exact versions, hashes, source pointers and limitations.', '',
|
|
'References:', 'https://doc.qt.io/qt-6/qtwebengine-licensing.html',
|
|
'https://doc.qt.io/qt-6/linux-deployment.html', '']
|
|
for name, item in sorted(components.items()):
|
|
lines += [f'{name} {item["version"]} [{item["distribution"]}]',
|
|
' Upstream: ' + item['upstreamHome'],
|
|
' Package license labels: ' + '; '.join(item.get('licenseLabelsFromPackage', ['See bundled PDFium notices'])),
|
|
' Source status: ' + item['source']['status']]
|
|
lines += [' License text: ' + text['path'] for text in item['licenseTexts']]
|
|
if not item['licenseTexts']:
|
|
lines.append(' License text: NOT COLLECTED from the installed package')
|
|
lines.append('')
|
|
(output / 'NOTICE.txt').write_text('\n'.join(lines), encoding='utf-8')
|
|
return result
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--prefix', type=Path, required=True)
|
|
parser.add_argument('--output', type=Path, required=True)
|
|
parser.add_argument('--qtpaths', default='/usr/lib/qt6/bin/qtpaths')
|
|
args = parser.parse_args()
|
|
result = inventory(args.prefix, args.output, args.qtpaths)
|
|
print(f'{len(result["components"])} components; {len(result["systemFiles"])} system runtime candidates; dependency sources not collected')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|