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>
|
034e0946 |
glregistry vendors
|
3969d955 |
glregistry type <type>
|
034e0946 |
glregistry aliases <enum>
|
3969d955 |
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>]
|
295d1b6e |
glregistry refs <name>
glregistry refs-all <name>
|
3969d955 |
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>.
|
034e0946 |
vendors
Print all vendor abbreviations.
|
3969d955 |
type <type>
Print the definition of <type>.
|
034e0946 |
aliases <enum>
Print the KHR, ARB, and EXT aliases of <enum>.
|
3969d955 |
value <enum>
Print the value of <enum>.
enum <value>
|
6248f9bd |
Print the enum(s) that has the given <value>.
|
3969d955 |
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.
|
295d1b6e |
refs <name>
Print all URLs of all reference pages with name <name>.
refs-all <name>
Print all URLs of all reference pages that mention <name>, sorted on
support, in a tree.
|
3969d955 |
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`.
|
320a682c |
GLREGISTRY_COLORS
If standard out is a terminal, the colors used in output of the `enums`,
`enums-tree`, `params`, `params-tree`, `audit,` and `audit-tree` commands.
It uses the same format (and defaults) as GREP_COLORS, i.e. a
colon-separated list of capabilties: `ms` (matching selected), `fn` (file
name), `ln` (line number), `se` (separators). Added custom capabilities
are: `ve` (version), `ex` (extension), `un` (unsupported). Defaults to
`ms=01;31:fn=35:ln=32:se=36:ve=01;34:ex=34:un=01;33`.
|
3969d955 |
"""
## Imports
import os
import sys
import collections
import re
|
3543cd70 |
import functools
|
3969d955 |
import subprocess
import shutil
import shlex
import urllib.request
import docopt
from lxml import etree
## Constants
|
295d1b6e |
REFPAGES_URL = 'https://registry.khronos.org/OpenGL-Refpages/'
|
3969d955 |
REGISTRY_URL = 'https://registry.khronos.org/OpenGL/'
|
295d1b6e |
REFPAGES_GIT = 'https://github.com/KhronosGroup/OpenGL-Refpages'
|
3969d955 |
XML_PATH = 'xml/gl.xml'
|
965e7beb |
USER_AGENT = 'Mozilla/5.0'
|
3969d955 |
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'
|
320a682c |
COLORS = collections.defaultdict(str, [
(color.split('=') + [''])[:2]
for color in
filter(None, (
os.environ.get('GLREGISTRY_COLORS') or
(lambda x, y: ':'.join([x, x and y]))(
(
os.environ.get('GREP_COLORS') or
'ms=01;31:fn=35:ln=32:se=36'
),
've=01;34:ex=34:un=01;33',
)
).split(':'))
])
|
3969d955 |
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')}]"
|
88ca5082 |
REMOVE = f"remove[{ MAYBE('api','gl')} and {MAYBE('profile','core')}]"
CHANGE_PREFIXES = [
[REQUIRE, '' ],
[REMOVE, '<'],
]
|
034e0946 |
VENDORS = ['KHR', 'ARB', 'EXT']
|
88ca5082 |
KEY_SUBS = [
|
034e0946 |
*[[f'^{ prefix}', f'' ] for _, prefix in CHANGE_PREFIXES],
*[[f'{ vendor}$', f'{ i}' ] for i, vendor in enumerate(VENDORS)],
*[[f'^GL_{vendor}_', f'GL_{i}_'] for i, vendor in enumerate(VENDORS)],
|
88ca5082 |
]
|
3969d955 |
## 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)
|
320a682c |
### `color`
def color(capability, string):
if not sys.stdout.isatty():
return string
return f'\x1b[{COLORS[capability]}m{string}\x1b[m'
### `color_supports`
def color_supports(supports):
for support in supports:
if support == 'UNSUPPORTED' or support.startswith('<'):
yield color('un', support)
elif support.startswith('GL_'):
yield color('ex', support)
else:
yield color('ve', support)
|
3969d955 |
### `indentjoin`
def indentjoin(indent, sep, parts):
|
320a682c |
return ' ' * INDENT * indent + color('se', sep).join(map(str, parts))
|
3969d955 |
### `removeprefix`
def removeprefix(prefix, string):
if string.startswith(prefix):
return string[len(prefix):]
return string
|
88ca5082 |
### `key`
def key(item):
for sub in KEY_SUBS:
item = re.sub(*sub, item)
return item
|
3969d955 |
### `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}")
|
034e0946 |
return sorted(exts, key=key)
|
3969d955 |
### `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', [], [])
)
|
034e0946 |
return sorted(exts_all, key=key)
### `vendors`
def vendors(xml):
vendors = set(extension.split('_')[1] for extension in exts(xml))
return sorted(vendors, key=key)
|
3969d955 |
### `type`
def type(xml, type):
return [xml.xpath(f"string({TYPES}/type/name[text()='{type}']/..)")]
|
034e0946 |
### `aliases`
def aliases(xml, name, supports_=None, vendors_=[]):
if not vendors_:
vendors_[:] = vendors(xml)
for vendor in vendors_:
if name.endswith(f'_{vendor}'):
return []
value_ = value(xml, name)
if not value_:
return []
if not supports_:
supports_ = supports(xml, name, False)
if not supports_:
return []
aliases = []
# for vendor in vendors_:
for vendor in VENDORS:
alias = f'{name}_{vendor}'
if supports(xml, alias, False) and value(xml, alias) == value_:
aliases.append(alias)
return sorted(aliases, key=key)
|
3969d955 |
### `value`
def value(xml, enum):
return xml.xpath(f"{ENUMS}/enum[@name='{enum}']/@value")
### `enum`
def enum(xml, value):
|
6248f9bd |
def conv(s):
return int(s, 16 if s.startswith('0x') else 10)
value = conv(value)
enum = (
enum.get('name')
for enum in xml.xpath(f"{ENUMS}/enum")
if conv(enum.get('value')) == value
)
|
034e0946 |
return sorted(enum, key=key)
|
3969d955 |
### `supports`
|
3543cd70 |
@functools.cache
|
034e0946 |
def supports(xml, name, use_aliases=True):
|
3969d955 |
category, attrib = CATEGORY_ATTRIBS['EXTENSION']
if xml.xpath(f"{category}[@{attrib}='{name}']"):
return ['EXTENSION']
supports_ = [
|
88ca5082 |
f'{prefix}{support}'
|
3969d955 |
for category, attrib in CATEGORY_ATTRIBS.values()
|
88ca5082 |
for change, prefix in CHANGE_PREFIXES
|
3969d955 |
for support in
|
88ca5082 |
xml.xpath(f"{category}/{change}/*[@name='{name}']/../../@{attrib}")
|
3969d955 |
]
|
034e0946 |
if supports_ and use_aliases:
supports_.extend(
support
for alias in aliases (xml, name, supports_)
for support in supports(xml, alias, False)
)
|
88ca5082 |
return sorted(supports_, key=key)
|
3969d955 |
### `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")
)
|
034e0946 |
return sorted(names, key=key)
|
3969d955 |
### `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
]
|
034e0946 |
return sorted(enums, key=key)
|
3969d955 |
### `enums_tree`
def enums_tree(xml, group=None):
for supports, enums in sorted(enums_(xml, group).items()):
|
320a682c |
yield indentjoin(0, ',', color_supports(supports))
|
3969d955 |
for enum in sorted(enums):
|
320a682c |
yield indentjoin(1, '', [color('ms', enum)])
|
3969d955 |
### `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, ':', [
|
320a682c |
color('ms', param),
color('ln', -count),
|
3969d955 |
])
for supports_, commands in sorted(occurences.items()):
|
320a682c |
yield indentjoin(1, ',', color_supports(supports_))
|
3969d955 |
for command in sorted(commands):
|
320a682c |
yield indentjoin(2, '', [color('fn', command)])
|
3969d955 |
### `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, ':', [
|
320a682c |
color('fn', file),
color('ln', line),
indentjoin(0, ',', color_supports(supports)),
color('ms', name),
|
3969d955 |
])
### `audit_tree`
def audit_tree(xml, path=None):
for supports, names in sorted(audit_(xml, path).items()):
|
320a682c |
yield indentjoin(0, ',', color_supports(supports))
|
3969d955 |
for name, locations in sorted(names.items()):
|
320a682c |
yield indentjoin(1, '', [color('ms', name)])
|
3969d955 |
for file, line in sorted(locations):
yield indentjoin(2, ':', [
|
320a682c |
color('fn', file),
color('ln', line),
|
3969d955 |
])
|
295d1b6e |
### `refs_`
def refs_(name):
local = os.path.join(CACHE, os.path.basename(REFPAGES_GIT))
if not os.path.exists(local):
os.makedirs(os.path.dirname(local), exist_ok=True)
subprocess.run(['git', 'clone', REFPAGES_GIT, local])
refs_ = collections.defaultdict(set)
for file, *_ in grep(local, rf'\b{name}\b', [], [], True):
file = removeprefix(f'{local}{os.path.sep}', file)
try:
support, *_, dir, base = os.path.normpath(file).split(os.path.sep)
except:
continue
if support.startswith('gl') and dir.endswith('html'):
support = removeprefix('gl', support)
name, ext = os.path.splitext(base)
url = urllib.parse.urljoin(REFPAGES_URL, file)
if ext in ['.xml', '.xhtml']:
refs_[support].add((name, url))
return refs_
### `refs`
def refs(name):
return sorted(
url
for support, locations in refs_(name).items()
for name_, url in locations
if name_ == name
)
### `refs_all`
def refs_all(name):
for support, locations in sorted(refs_(name).items()):
|
320a682c |
yield indentjoin(0, ',', color_supports([support]))
|
295d1b6e |
for name_, url in sorted(locations):
yield indentjoin(1, ':', [
|
320a682c |
color('ms', name_),
color('fn', url),
|
295d1b6e |
])
|
3969d955 |
## Main
def main():
|
965e7beb |
opener = urllib.request.build_opener()
opener.addheaders = [('User-Agent', USER_AGENT)]
urllib.request.install_opener(opener)
|
3969d955 |
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>']))
|
034e0946 |
if args['vendors']: page(vendors (xml_()))
|
3969d955 |
if args['type']: page(type (xml_(), args['<type>']))
|
034e0946 |
if args['aliases']: page(aliases (xml_(), args['<enum>']))
|
3969d955 |
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>']))
|
295d1b6e |
if args['refs']: page(refs (args['<name>']))
if args['refs-all']: page(refs_all (args['<name>']))
|
3969d955 |
if __name__ == '__main__':
main()
|