mirror of
https://github.com/qpdf/qpdf.git
synced 2024-12-22 02:49:00 +00:00
53ba65eb59
Handle optional choices in addition to required choices. Refactor the way help options are added to completion to make it work with optional help choices.
241 lines
9.2 KiB
Python
Executable File
241 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import hashlib
|
|
import re
|
|
import yaml
|
|
|
|
whoami = os.path.basename(sys.argv[0])
|
|
BANNER = f'''//
|
|
// This file is automatically generated by {whoami}.
|
|
// Edits will be automatically overwritten if the build is
|
|
// run in maintainer mode.
|
|
//'''
|
|
|
|
|
|
def warn(*args, **kwargs):
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
class Main:
|
|
SOURCES = [whoami, 'job.yml']
|
|
DESTS = {
|
|
'decl': 'libqpdf/qpdf/auto_job_decl.hh',
|
|
'init': 'libqpdf/qpdf/auto_job_init.hh',
|
|
}
|
|
SUMS = 'job.sums'
|
|
|
|
def main(self, args=sys.argv[1:], prog=whoami):
|
|
options = self.parse_args(args, prog)
|
|
self.top(options)
|
|
|
|
def parse_args(self, args, prog):
|
|
parser = argparse.ArgumentParser(
|
|
prog=prog,
|
|
description='Generate files for QPDFJob',
|
|
)
|
|
mxg = parser.add_mutually_exclusive_group(required=True)
|
|
mxg.add_argument('--check',
|
|
help='update checksums if files are not up to date',
|
|
action='store_true', default=False)
|
|
mxg.add_argument('--generate',
|
|
help='generate files from sources',
|
|
action='store_true', default=False)
|
|
return parser.parse_args(args)
|
|
|
|
def top(self, options):
|
|
if options.check:
|
|
self.check()
|
|
elif options.generate:
|
|
self.generate()
|
|
else:
|
|
exit(f'{whoami} unknown mode')
|
|
|
|
def get_hashes(self):
|
|
hashes = {}
|
|
for i in sorted([*self.SOURCES, *self.DESTS.values()]):
|
|
m = hashlib.sha256()
|
|
try:
|
|
with open(i, 'rb') as f:
|
|
m.update(f.read())
|
|
hashes[i] = m.hexdigest()
|
|
except FileNotFoundError:
|
|
pass
|
|
return hashes
|
|
|
|
def check(self):
|
|
hashes = self.get_hashes()
|
|
match = False
|
|
try:
|
|
old_hashes = {}
|
|
with open(self.SUMS, 'r') as f:
|
|
for line in f.readlines():
|
|
m = re.match(r'^(\S+) (\S+)\s*$', line)
|
|
if m:
|
|
old_hashes[m.group(1)] = m.group(2)
|
|
match = old_hashes == hashes
|
|
except Exception:
|
|
pass
|
|
if not match:
|
|
exit(f'{whoami}: auto job inputs have changed')
|
|
|
|
def update_hashes(self):
|
|
hashes = self.get_hashes()
|
|
with open(self.SUMS, 'w') as f:
|
|
print(f'# Generated by {whoami}', file=f)
|
|
for k, v in hashes.items():
|
|
print(f'{k} {v}', file=f)
|
|
|
|
def generate(self):
|
|
warn(f'{whoami}: regenerating auto job files')
|
|
|
|
with open('job.yml', 'r') as f:
|
|
data = yaml.safe_load(f.read())
|
|
self.validate(data)
|
|
with open(self.DESTS['decl'], 'w') as f:
|
|
print(BANNER, file=f)
|
|
self.generate_decl(data, f)
|
|
with open(self.DESTS['init'], 'w') as f:
|
|
print(BANNER, file=f)
|
|
self.generate_init(data, f)
|
|
|
|
# Update hashes last to ensure that this will be rerun in the
|
|
# event of a failure.
|
|
self.update_hashes()
|
|
# DON'T ADD CODE TO generate AFTER update_hashes
|
|
|
|
def check_keys(self, what, d, exp):
|
|
if not isinstance(d, dict):
|
|
exit(f'{what} is not a dictionary')
|
|
actual = set(d.keys())
|
|
extra = actual - exp
|
|
if extra:
|
|
exit(f'{what}: unknown keys = {extra}')
|
|
|
|
def validate(self, data):
|
|
self.check_keys('top', data, set(['choices', 'options']))
|
|
for o in data['options']:
|
|
self.check_keys('top', o, set(
|
|
['table', 'prefix', 'bare', 'positional',
|
|
'optional_parameter', 'required_parameter',
|
|
'required_choices', 'optional_choices', 'from_table']))
|
|
|
|
def to_identifier(self, label, prefix, const):
|
|
identifier = re.sub(r'[^a-zA-Z0-9]', '_', label)
|
|
if const:
|
|
identifier = identifier.upper()
|
|
else:
|
|
identifier = identifier.lower()
|
|
identifier = re.sub(r'(?:^|_)([a-z])',
|
|
lambda x: x.group(1).upper(),
|
|
identifier).replace('_', '')
|
|
return prefix + identifier
|
|
|
|
def generate_decl(self, data, f):
|
|
for o in data['options']:
|
|
table = o['table']
|
|
if table in ('main', 'help'):
|
|
continue
|
|
i = self.to_identifier(table, 'O_', True)
|
|
print(f'static constexpr char const* {i} = "{table}";', file=f)
|
|
print('', file=f)
|
|
for o in data['options']:
|
|
table = o['table']
|
|
prefix = 'arg' + o.get('prefix', '')
|
|
if o.get('positional', False):
|
|
print(f'void {prefix}Positional(char*);', file=f)
|
|
for i in o.get('bare', []):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'void {identifier}();', file=f)
|
|
for i in o.get('optional_parameter', []):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'void {identifier}(char *);', file=f)
|
|
for i in o.get('required_parameter', {}):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'void {identifier}(char *);', file=f)
|
|
for i in o.get('required_choices', {}):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'void {identifier}(char *);', file=f)
|
|
for i in o.get('optional_choices', {}):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'void {identifier}(char *);', file=f)
|
|
if table not in ('main', 'help'):
|
|
identifier = self.to_identifier(table, 'argEnd', False)
|
|
print(f'void {identifier}();', file=f)
|
|
|
|
def generate_init(self, data, f):
|
|
print('auto b = [this](void (ArgParser::*f)()) {', file=f)
|
|
print(' return QPDFArgParser::bindBare(f, this);', file=f)
|
|
print('};', file=f)
|
|
print('auto p = [this](void (ArgParser::*f)(char *)) {', file=f)
|
|
print(' return QPDFArgParser::bindParam(f, this);', file=f)
|
|
print('};', file=f)
|
|
print('', file=f)
|
|
for k, v in data['choices'].items():
|
|
print(f'char const* {k}_choices[] = {{', file=f, end='')
|
|
for i in v:
|
|
print(f'"{i}", ', file=f, end='')
|
|
print('0};', file=f)
|
|
print('', file=f)
|
|
for o in data['options']:
|
|
table = o['table']
|
|
if table == 'main':
|
|
print('this->ap.selectMainOptionTable();', file=f)
|
|
elif table == 'help':
|
|
print('this->ap.selectHelpOptionTable();', file=f)
|
|
else:
|
|
identifier = self.to_identifier(table, 'argEnd', False)
|
|
print(f'this->ap.registerOptionTable("{table}",'
|
|
f' b(&ArgParser::{identifier}));', file=f)
|
|
prefix = 'arg' + o.get('prefix', '')
|
|
if o.get('positional', False):
|
|
print('this->ap.addPositional('
|
|
f'p(&ArgParser::{prefix}Positional));', file=f)
|
|
for i in o.get('bare', []):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'this->ap.addBare("{i}", '
|
|
f'b(&ArgParser::{identifier}));', file=f)
|
|
for i in o.get('optional_parameter', []):
|
|
identifier = self.to_identifier(i, prefix, False)
|
|
print(f'this->ap.addOptionalParameter("{i}", '
|
|
f'p(&ArgParser::{identifier}));', file=f)
|
|
for k, v in o.get('required_parameter', {}).items():
|
|
identifier = self.to_identifier(k, prefix, False)
|
|
print(f'this->ap.addRequiredParameter("{k}", '
|
|
f'p(&ArgParser::{identifier})'
|
|
f', "{v}");', file=f)
|
|
for k, v in o.get('required_choices', {}).items():
|
|
identifier = self.to_identifier(k, prefix, False)
|
|
print(f'this->ap.addChoices("{k}", '
|
|
f'p(&ArgParser::{identifier})'
|
|
f', true, {v}_choices);', file=f)
|
|
for k, v in o.get('optional_choices', {}).items():
|
|
identifier = self.to_identifier(k, prefix, False)
|
|
print(f'this->ap.addChoices("{k}", '
|
|
f'p(&ArgParser::{identifier})'
|
|
f', false, {v}_choices);', file=f)
|
|
for o in data['options']:
|
|
table = o['table']
|
|
if 'from_table' not in o:
|
|
continue
|
|
if table == 'main':
|
|
print('this->ap.selectMainOptionTable();', file=f)
|
|
elif table == 'help':
|
|
print('this->ap.selectHelpOptionTable();', file=f)
|
|
else:
|
|
print(f'this->ap.selectOptionTable("{table}");', file=f)
|
|
ft = o['from_table']
|
|
other_table = ft['table']
|
|
for j in ft['options']:
|
|
print('this->ap.copyFromOtherTable'
|
|
f'("{j}", "{other_table}");', file=f)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
|
Main().main()
|
|
except KeyboardInterrupt:
|
|
exit(130)
|