forked from openkylin/package-notes
223 lines
8.9 KiB
Python
Executable File
223 lines
8.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: CC0-1.0
|
|
|
|
"""
|
|
Generates a linker script to insert a .note.package section with a
|
|
JSON payload. The contents are derived from the specified options and the
|
|
os-release file. Use the output with -Wl,-dT,/path/to/output in $LDFLAGS.
|
|
|
|
$ ./generate-package-notes.py --package-type rpm --package-name systemd --package-version 248~rc2-1.fc34 --package-architecture x86_64 --cpe 'cpe:/o:fedoraproject:fedora:33'
|
|
SECTIONS
|
|
{
|
|
.note.package (READONLY) : ALIGN(4) {
|
|
BYTE(0x04) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Owner including NUL */
|
|
BYTE(0x7c) BYTE(0x00) BYTE(0x00) BYTE(0x00) /* Length of Value including NUL */
|
|
BYTE(0x7e) BYTE(0x1a) BYTE(0xfe) BYTE(0xca) /* Note ID */
|
|
BYTE(0x46) BYTE(0x44) BYTE(0x4f) BYTE(0x00) /* Owner: 'FDO\x00' */
|
|
BYTE(0x7b) BYTE(0x22) BYTE(0x74) BYTE(0x79) /* Value: '{"type":"rpm","name":"systemd","version":"248~rc2-1.fc34","architecture":"x86_64","osCpe":"cpe:/o:fedoraproject:fedora:33"}\x00' */
|
|
BYTE(0x70) BYTE(0x65) BYTE(0x22) BYTE(0x3a)
|
|
BYTE(0x22) BYTE(0x72) BYTE(0x70) BYTE(0x6d)
|
|
BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6e)
|
|
BYTE(0x61) BYTE(0x6d) BYTE(0x65) BYTE(0x22)
|
|
BYTE(0x3a) BYTE(0x22) BYTE(0x73) BYTE(0x79)
|
|
BYTE(0x73) BYTE(0x74) BYTE(0x65) BYTE(0x6d)
|
|
BYTE(0x64) BYTE(0x22) BYTE(0x2c) BYTE(0x22)
|
|
BYTE(0x76) BYTE(0x65) BYTE(0x72) BYTE(0x73)
|
|
BYTE(0x69) BYTE(0x6f) BYTE(0x6e) BYTE(0x22)
|
|
BYTE(0x3a) BYTE(0x22) BYTE(0x32) BYTE(0x34)
|
|
BYTE(0x38) BYTE(0x7e) BYTE(0x72) BYTE(0x63)
|
|
BYTE(0x32) BYTE(0x2d) BYTE(0x31) BYTE(0x2e)
|
|
BYTE(0x66) BYTE(0x63) BYTE(0x33) BYTE(0x34)
|
|
BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x61)
|
|
BYTE(0x72) BYTE(0x63) BYTE(0x68) BYTE(0x69)
|
|
BYTE(0x74) BYTE(0x65) BYTE(0x63) BYTE(0x74)
|
|
BYTE(0x75) BYTE(0x72) BYTE(0x65) BYTE(0x22)
|
|
BYTE(0x3a) BYTE(0x22) BYTE(0x78) BYTE(0x38)
|
|
BYTE(0x36) BYTE(0x5f) BYTE(0x36) BYTE(0x34)
|
|
BYTE(0x22) BYTE(0x2c) BYTE(0x22) BYTE(0x6f)
|
|
BYTE(0x73) BYTE(0x43) BYTE(0x70) BYTE(0x65)
|
|
BYTE(0x22) BYTE(0x3a) BYTE(0x22) BYTE(0x63)
|
|
BYTE(0x70) BYTE(0x65) BYTE(0x3a) BYTE(0x2f)
|
|
BYTE(0x6f) BYTE(0x3a) BYTE(0x66) BYTE(0x65)
|
|
BYTE(0x64) BYTE(0x6f) BYTE(0x72) BYTE(0x61)
|
|
BYTE(0x70) BYTE(0x72) BYTE(0x6f) BYTE(0x6a)
|
|
BYTE(0x65) BYTE(0x63) BYTE(0x74) BYTE(0x3a)
|
|
BYTE(0x66) BYTE(0x65) BYTE(0x64) BYTE(0x6f)
|
|
BYTE(0x72) BYTE(0x61) BYTE(0x3a) BYTE(0x33)
|
|
BYTE(0x33) BYTE(0x22) BYTE(0x7d) BYTE(0x00)
|
|
}
|
|
}
|
|
INSERT AFTER .note.gnu.build-id;
|
|
/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */
|
|
|
|
See https://systemd.io/COREDUMP_PACKAGE_METADATA/ for details.
|
|
"""
|
|
|
|
__version__ = '0.8'
|
|
|
|
import argparse
|
|
import itertools
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import simplejson as json
|
|
|
|
DOC_PARAGRAPHS = ['\n'.join(group)
|
|
for (key, group) in itertools.groupby(__doc__.splitlines(), bool)
|
|
if key]
|
|
|
|
def read_os_release(field, root=Path('/')):
|
|
try:
|
|
f = open(root / 'etc/os-release')
|
|
except FileNotFoundError:
|
|
f = open(root / 'usr/lib/os-release')
|
|
|
|
prefix = '{}='.format(field)
|
|
for line in f:
|
|
if line.startswith(prefix):
|
|
break
|
|
else:
|
|
return None
|
|
|
|
value = line.rstrip()
|
|
value = value[value.startswith(prefix) and len(prefix):]
|
|
if value[0] in '"\'' and value[0] == value[-1]:
|
|
value = value[1:-1]
|
|
|
|
return value
|
|
|
|
def str_to_bool(v):
|
|
if isinstance(v, bool):
|
|
return v
|
|
if v.lower() in {'yes', 'true', '1'}:
|
|
return True
|
|
if v.lower() in {'no', 'false', '0'}:
|
|
return False
|
|
raise argparse.ArgumentTypeError('"yes"/"true"/"1"/"no"/"false"/"0" expected')
|
|
|
|
def parse_args():
|
|
p = argparse.ArgumentParser(description=DOC_PARAGRAPHS[0],
|
|
epilog=DOC_PARAGRAPHS[-1],
|
|
allow_abbrev=False)
|
|
p.add_argument('--package-type', metavar='TYPE',
|
|
default='package',
|
|
help='Specify the package type, e.g. "rpm" or "deb"')
|
|
p.add_argument('--package-name', metavar='NAME',
|
|
help='The name of the package (e.g. "foo" or "libbar")')
|
|
p.add_argument('--package-version', metavar='VERSION',
|
|
help='The full version of the package (e.g. 1.5-1.fc35.s390x)')
|
|
p.add_argument('--package-architecture', metavar='ARCH',
|
|
help='The code architecture of the binaries (e.g. arm64 or s390x)')
|
|
p.add_argument('--cpe',
|
|
help='NIST CPE identifier of the vendor operating system, or \'auto\' to parse from system-release-cpe or os-release')
|
|
p.add_argument('--rpm', metavar='NEVRA',
|
|
help='Extract type,name,version,architecture from a full rpm name')
|
|
p.add_argument('--debug-info-url', metavar='URL',
|
|
help='URL of the debuginfod server where sources can be queried')
|
|
p.add_argument('--readonly', metavar='BOOL',
|
|
type=str_to_bool, default=True,
|
|
help='Make the notes section read-only (requires binutils 2.38)')
|
|
p.add_argument('--root', metavar='PATH', type=Path, default="/",
|
|
help='When a file (eg: /usr/lib/os-release) is parsed, open it relatively from this hierarchy')
|
|
p.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
|
|
|
opts = p.parse_args()
|
|
|
|
return opts
|
|
|
|
def encode_bytes(arr):
|
|
return ' '.join('BYTE(0x{:02x})'.format(n) for n in arr)
|
|
|
|
def encode_bytes_lines(arr, prefix='', label='string'):
|
|
assert len(arr) % 4 == 0
|
|
s = bytes(arr).decode()
|
|
yield prefix + encode_bytes(arr[:4]) + ' /* {}: {!r} */'.format(label, s)
|
|
for offset in range(4, len(arr), 4):
|
|
yield prefix + encode_bytes(arr[offset:offset+4])
|
|
|
|
def encode_length(s, prefix='', label='string'):
|
|
n = (len(s) + 1) * 4 // 4
|
|
return f'{prefix}LONG(0x{n:04x}) /* Length of {label} including NUL */'
|
|
|
|
def encode_note_id(id, prefix=''):
|
|
return f'{prefix}LONG(0x{id:04x}) /* Note ID */'
|
|
|
|
def pad_string(s):
|
|
return [0] * ((len(s) + 4) // 4 * 4 - len(s))
|
|
|
|
def encode_string(s, prefix='', label='string'):
|
|
arr = list(s.encode()) + pad_string(s)
|
|
yield from encode_bytes_lines(arr, prefix=prefix, label=label)
|
|
|
|
def encode_note(note_name, note_id, owner, value, readonly=True, prefix=''):
|
|
l1 = encode_length(owner, prefix=prefix + ' ', label='Owner')
|
|
l2 = encode_length(value, prefix=prefix + ' ', label='Value')
|
|
l3 = encode_note_id(note_id, prefix=prefix + ' ')
|
|
l4 = encode_string(owner, prefix=prefix + ' ', label='Owner')
|
|
l5 = encode_string(value, prefix=prefix + ' ', label='Value')
|
|
readonly = '(READONLY) ' if readonly else ''
|
|
|
|
return [prefix + '.note.{} {}: ALIGN(4) {{'.format(note_name, readonly),
|
|
l1, l2, l3, *l4, *l5,
|
|
prefix + '}']
|
|
|
|
NOTE_ID= 0xcafe1a7e
|
|
|
|
def json_serialize(s):
|
|
# Avoid taking space in the ELF header if there's no value to store
|
|
return json.dumps({k: v for k, v in s.items() if v is not None},
|
|
ensure_ascii=False,
|
|
separators=(',', ':'))
|
|
|
|
def gather_data(opts):
|
|
if opts.cpe == 'auto':
|
|
try:
|
|
with open(Path(opts.root, 'usr/lib/system-release-cpe'), 'r') as f:
|
|
opts.cpe = f.read()
|
|
except FileNotFoundError:
|
|
opts.cpe = read_os_release('CPE_NAME', root=opts.root)
|
|
if opts.cpe is None or opts.cpe == "":
|
|
raise ValueError(f"Could not read {opts.root}usr/lib/system-release-cpe or CPE_NAME from {opts.root}usr/lib/os-release")
|
|
|
|
if opts.rpm:
|
|
split = re.match(r'(.*?)-([0-9].*)\.(.*)', opts.rpm)
|
|
if not split:
|
|
raise ValueError('{!r} does not seem to be a valid package name'.format(opts.rpm))
|
|
opts.package_type = 'rpm'
|
|
opts.package_name = split.group(1)
|
|
opts.package_version = split.group(2)
|
|
opts.package_architecture = split.group(3)
|
|
|
|
data = {
|
|
'type': opts.package_type,
|
|
'name': opts.package_name,
|
|
'version': opts.package_version,
|
|
'architecture': opts.package_architecture,
|
|
}
|
|
if opts.cpe:
|
|
data['osCpe'] = opts.cpe
|
|
else:
|
|
data['os'] = read_os_release('ID', root=opts.root)
|
|
data['osVersion'] = read_os_release('VERSION_ID', root=opts.root)
|
|
if opts.debug_info_url:
|
|
data['debugInfoUrl'] = opts.debug_info_url
|
|
return data
|
|
|
|
def generate_section(data, readonly=True):
|
|
json = json_serialize(data)
|
|
|
|
section = encode_note('package', NOTE_ID, 'FDO', json, readonly=readonly, prefix=' ')
|
|
return ['SECTIONS', '{',
|
|
*section,
|
|
'}',
|
|
'INSERT AFTER .note.gnu.build-id;',
|
|
'/* HINT: add -Wl,-dT,/path/to/this/file to $LDFLAGS */']
|
|
|
|
if __name__ == '__main__':
|
|
opts = parse_args()
|
|
data = gather_data(opts)
|
|
lines = generate_section(data, readonly=opts.readonly)
|
|
|
|
print('\n'.join(lines))
|