3969d955 |
#!/usr/bin/env python3
## Help
"""
glregistry 1.0
Cache and query the OpenGL registry locally.
Usage:
glregistry xml
glregistry xml-path
glregistry ext <extension>
glregistry ext-path <extension>
glregistry exts
glregistry exts-download
glregistry exts-all <name>
glregistry type <type>
glregistry value <enum>
glregistry enum <value>
glregistry supports <name>
glregistry names [<support>]
glregistry groups [<enum>]
glregistry enums [<group>]
glregistry enums-tree [<group>]
glregistry params [<group>]
glregistry params-tree [<group>]
glregistry audit [<path>]
glregistry audit-tree [<path>]
glregistry -h|--help
Commands:
xml
Download the registry XML and open it with an editor.
xml-path
Download the registry XML and print its local path.
ext <extension>
Download the <extension> spec and open it with an editor.
ext-path <extension>
Download the <extension> spec and print its local path.
exts
Print the names of all extension specs.
exts-download
Download all extension specs.
exts-all <name>
Print all downloaded extensions that mention <name>.
type <type>
Print the definition of <type>.
value <enum>
Print the value of <enum>.
enum <value>
Print the enum(s) that has the given <value>, using exact string matching.
supports <name>
Print the OpenGL version or extension required to use <name>.
names [<support>]
Print the names introduced by the OpenGL version or extension <support> if
given, or all names if omitted. The special values VERSION and EXTENSION
print the names introduced by all versions or all extensions respectively.
groups [<enum>]
Print the groups of <enum> if given, or all groups if omitted.
enums [<group>]
Print the enums in <group> if given, or all enums if omitted.
enums-tree [<group>]
Print the enums in <group> if given, or all enums if omitted, sorted on
support, in a tree.
params [<group>]
Print the parameter names of <group> if given, or all parameter names if
omitted.
params-tree [<group>]
Print the parameter names of <group> if given, or all parameter names if
omitted, sorted on count, together with the commands sorted on support, in
a tree.
audit [<path>]
Search files in <path> if given, or the current directory if omitted,
recursively for OpenGL API names and print them sorted on location,
support, and name, in a list.
audit-tree [<path>]
Search files in <path> if given, or the current directory if omitted,
recursively for OpenGL API names and print them sorted on support, name,
and location, in a tree.
Environment variables:
GLREGISTRY_CACHE
The directory to cache files in. Defaults to `$XDG_CACHE_HOME/glregistry`
or, if `$XDG_CACHE_HOME` is not defined, `$HOME/.cache/glregistry`.
GLREGISTRY_EDITOR
The editor to use when opening files. Defaults to `$EDITOR` or, if
`$EDITOR` is not defined, `editor` if it exists in `$PATH`, else `vi`. The
value is interpreted by the shell.
GLREGISTRY_PAGER
The pager to use when viewing output. Defaults to `$PAGER` or, if `$PAGER`
is not defined, `pager` if it exists in `$PATH`, else `less` . The value is
interpreted by the shell. If the `$LESS` environment variable is unset, it
is set to `FR`.
"""
## Imports
import os
import sys
import collections
import re
import subprocess
import shutil
import shlex
import urllib.request
import docopt
from lxml import etree
## Constants
REGISTRY_URL = 'https://registry.khronos.org/OpenGL/'
XML_PATH = 'xml/gl.xml'
REGEX = r'\b(gl|GL_)[0-9A-Z][0-9A-Za-z_]+\b'
EXCLUDE_DIRS = ['.?*', '_*']
EXCLUDE_FILES = ['README*', 'TODO*']
INDENT = 2
ENV_XDG = lambda var, default: (
os.environ.get(f'GLREGISTRY_{var}') or
os.path.join(
(
os.environ.get(f'XDG_{var}_HOME') or
os.path.expanduser(default)
),
'glregistry',
)
)
ENV_PRG = lambda var, default: (
os.environ.get(f'GLREGISTRY_{var}') or
os.environ.get(var) or
shutil.which(var.lower()) or
default
)
CACHE = ENV_XDG('CACHE', os.path.join('~', '.cache'))
EDITOR = ENV_PRG('EDITOR', 'vi')
PAGER = ENV_PRG('PAGER', 'less')
LESS = os.environ.get('LESS') or 'FR'
IN = lambda a, v, s: f"contains(concat('{s}',@{a},'{s}'),'{s}{v}{s}')"
MAYBE = lambda a, v: f"(@{a}='{v}' or not(@{a}))"
TYPES = "/registry/types"
ENUMS = "/registry/enums"
COMMANDS = "/registry/commands"
CATEGORY_ATTRIBS = {
'VERSION': [
"/registry/feature[@api='gl']",
'number',
],
'EXTENSION': [
f"/registry/extensions/extension[{IN('supported','gl','|')}]",
'name',
],
}
REQUIRE = f"require[{MAYBE('api','gl')} and {MAYBE('profile','core')}]"
## Helpers
### `log`
def log(*args, **kwargs):
print(*args, file=sys.stderr, flush=True, **kwargs)
### `edit`
def edit(paths):
if EDITOR and sys.stdout.isatty():
args = ' '.join([EDITOR, *map(shlex.quote, paths)])
subprocess.run(args, shell=True)
else:
for path in paths:
with open(path) as f:
shutil.copyfileobj(f, sys.stdout)
### `page`
def page(lines):
lines = ''.join(f'{line}\n' for line in lines)
if lines and PAGER and sys.stdout.isatty():
args = f'LESS={shlex.quote(LESS)} {PAGER}'
subprocess.run(args, shell=True, text=True, input=lines)
else:
sys.stdout.write(lines)
### `indentjoin`
def indentjoin(indent, sep, parts):
return ' ' * INDENT * indent + sep.join(map(str, parts))
### `removeprefix`
def removeprefix(prefix, string):
if string.startswith(prefix):
return string[len(prefix):]
return string
### `download`
def download(path, exit_on_failure=True):
remote = urllib.parse.urljoin(REGISTRY_URL, path)
local = os.path.join (CACHE, path)
if not os.path.exists(local):
try:
log(f"Downloading '{path}' ... ", end='')
with urllib.request.urlopen(remote) as response:
os.makedirs(os.path.dirname(local), exist_ok=True)
with open(local, 'wb') as f:
shutil.copyfileobj(response, f)
except urllib.error.URLError as error:
log(error.reason)
if exit_on_failure:
exit(1)
else:
log(response.reason)
return local
### `grep`
def grep(
path=None,
regex=REGEX,
exclude_dirs=EXCLUDE_DIRS,
exclude_files=EXCLUDE_FILES,
silent=False,
):
path = path if path else '.'
cmd = ['grep', '-EIrno']
exclude_dirs = [f'--exclude-dir={exclude}' for exclude in exclude_dirs]
exclude_files = [f'--exclude={exclude}' for exclude in exclude_files]
process = subprocess.run(
[*cmd, *exclude_dirs, *exclude_files, regex, path],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL if silent else None,
text=True,
)
for string in process.stdout.splitlines():
string = removeprefix(f'.{os.path.sep}', string)
file, line, name = string.split(':', 2)
line = int(line)
yield file, line, name
## Commands
### `xml_`
def xml_():
return etree.parse(download(XML_PATH))
### `xml`
def xml():
return [download(XML_PATH)]
### `ext_`
def ext_(extension):
prefix, vendor, name = extension.split('_', 2)
if prefix != 'GL':
log("Extension names must start with 'GL_'.")
exit(1)
return f'extensions/{vendor}/{vendor}_{name}.txt'
### `ext`
def ext(extension):
return [download(ext_(extension))]
### `exts`
def exts(xml):
category, attrib = CATEGORY_ATTRIBS['EXTENSION']
exts = xml.xpath(f"{category}/@{attrib}")
return sorted(exts)
### `exts_download`
def exts_download(xml):
for ext in exts(xml):
download(ext_(ext), exit_on_failure=False)
return []
### `exts_all`
def exts_all(xml, name):
# exts_download(xml)
exts_all = set(
'GL_' + os.path.splitext(os.path.basename(file))[0]
for name in set([
name,
removeprefix('gl', name),
removeprefix('GL_', name),
])
for file, *_ in
grep(os.path.join(CACHE, 'extensions'), rf'\b{name}\b', [], [])
)
return sorted(exts_all)
### `type`
def type(xml, type):
return [xml.xpath(f"string({TYPES}/type/name[text()='{type}']/..)")]
### `value`
def value(xml, enum):
return xml.xpath(f"{ENUMS}/enum[@name='{enum}']/@value")
### `enum`
def enum(xml, value):
enum = xml.xpath(f"{ENUMS}/enum[@value='{value}']/@name")
return sorted(enum)
### `supports`
def supports(xml, name):
category, attrib = CATEGORY_ATTRIBS['EXTENSION']
if xml.xpath(f"{category}[@{attrib}='{name}']"):
return ['EXTENSION']
supports_ = [
support
for category, attrib in CATEGORY_ATTRIBS.values()
for support in
xml.xpath(f"{category}/{REQUIRE}/*[@name='{name}']/../../@{attrib}")
]
return sorted(supports_)
### `names`
def names(xml, support=None):
if support in CATEGORY_ATTRIBS.keys():
category_attribs = [
[category, ""]
for category, _ in [CATEGORY_ATTRIBS[support]]
]
elif support:
category_attribs = [
[category, f"[@{attrib}='{support}']"]
for category, attrib in CATEGORY_ATTRIBS.values()
]
else:
category_attribs = [
[category, ""]
for category, _ in CATEGORY_ATTRIBS.values()
]
names = set(
name
for category, attrib in category_attribs
for name in
xml.xpath(f"{category}{attrib}/{REQUIRE}/*/@name")
)
return sorted(names)
### `groups`
def groups(xml, enum=None):
name = f"[@name='{enum}']" if enum else ""
return sorted(set(
group
for groups in xml.xpath(f"{ENUMS}/enum{name}/@group")
for group in groups.split(',')
))
### `enums_`
def enums_(xml, group=None):
group = f"[{IN('group',group,',')}]" if group else ""
enums_ = collections.defaultdict(list)
for enum in xml.xpath(f"{ENUMS}/enum{group}/@name"):
supports_ = supports(xml, enum)
if supports_:
enums_[tuple(supports_)].append(enum)
return enums_
### `enums`
def enums(xml, group=None):
enums = [
enum
for _, enums in enums_(xml, group).items()
for enum in enums
]
return sorted(enums)
### `enums_tree`
def enums_tree(xml, group=None):
for supports, enums in sorted(enums_(xml, group).items()):
yield indentjoin(0, ',', supports)
for enum in sorted(enums):
yield indentjoin(1, '', [enum])
### `params_`
def params_(xml, group=None):
group = f"[@group='{group}']" if group else ""
counts = collections.defaultdict(int)
params_ = collections.defaultdict(lambda: collections.defaultdict(list))
for xmlcommand in xml.xpath(f"{COMMANDS}/command/param{group}/.."):
command = xmlcommand.xpath(f"string(proto/name)")
supports_ = supports(xml, command)
if supports_:
for xmlparam in xmlcommand.xpath(f"param{group}"):
param = xmlparam.xpath(f"string(name)")
params_[param][tuple(supports_)].append(command)
counts[param] -= 1
return {
(count, param): params_[param]
for param, count in counts.items()
}
### `params`
def params(xml, group=None):
params = [param for (_, param), _ in params_(xml, group).items()]
return sorted(params)
### `params_tree`
def params_tree(xml, group=None):
for (count, param), occurences in sorted(params_(xml, group).items()):
yield indentjoin(0, ':', [
param,
-count,
])
for supports_, commands in sorted(occurences.items()):
yield indentjoin(1, ',', supports_)
for command in sorted(commands):
yield indentjoin(2, '', [command])
### `audit_`
def audit_(xml, path=None):
audit_ = collections.defaultdict(lambda: collections.defaultdict(list))
for file, line, name in grep(path):
supports_ = supports(xml, name)
if not supports_:
supports_ = ['UNSUPPORTED']
audit_[tuple(supports_)][name].append([file, line])
return audit_
### `audit`
def audit(xml, path=None):
for file, line, supports, name in sorted(
[file, line, supports, name]
for supports, names in audit_(xml, path).items()
for name, locations in names.items()
for file, line in locations
):
yield indentjoin(0, ':', [
file,
line,
indentjoin(0, ',', supports),
name
])
### `audit_tree`
def audit_tree(xml, path=None):
for supports, names in sorted(audit_(xml, path).items()):
yield indentjoin(0, ',', supports)
for name, locations in sorted(names.items()):
yield indentjoin(1, '', [name])
for file, line in sorted(locations):
yield indentjoin(2, ':', [
file,
line,
])
## Main
def main():
args = docopt.docopt(__doc__)
if args['xml']: edit(xml ())
if args['xml-path']: page(xml ())
if args['ext']: edit(ext (args['<extension>']))
if args['ext-path']: page(ext (args['<extension>']))
if args['exts']: page(exts (xml_()))
if args['exts-download']: page(exts_download(xml_()))
if args['exts-all']: page(exts_all (xml_(), args['<name>']))
if args['type']: page(type (xml_(), args['<type>']))
if args['value']: page(value (xml_(), args['<enum>']))
if args['enum']: page(enum (xml_(), args['<value>']))
if args['supports']: page(supports (xml_(), args['<name>']))
if args['names']: page(names (xml_(), args['<support>']))
if args['groups']: page(groups (xml_(), args['<enum>']))
if args['enums']: page(enums (xml_(), args['<group>']))
if args['enums-tree']: page(enums_tree (xml_(), args['<group>']))
if args['params']: page(params (xml_(), args['<group>']))
if args['params-tree']: page(params_tree (xml_(), args['<group>']))
if args['audit']: page(audit (xml_(), args['<path>']))
if args['audit-tree']: page(audit_tree (xml_(), args['<path>']))
if __name__ == '__main__':
main()
|