2019-05-08 16:48:37 -05:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
#
|
|
|
|
|
# Copyright (c) 2018 Collabora, Ltd.
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
#
|
|
|
|
|
# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com>
|
|
|
|
|
#
|
|
|
|
|
# Purpose: This script searches for and extracts embedded source code
|
|
|
|
|
# from specification chapters.
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import errno
|
|
|
|
|
import re
|
|
|
|
|
from enum import Enum, unique
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
from spec_tools.file_process import LinewiseFileProcessor
|
|
|
|
|
|
2019-05-08 16:48:37 -05:00
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
ALL_DOCS = sorted((ROOT / 'specification/sources/').glob('**/*.adoc'))
|
|
|
|
|
|
|
|
|
|
CODEDIR = ROOT / 'specification/example-builds'
|
|
|
|
|
GENCODEDIR = CODEDIR / 'generated'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@unique
|
|
|
|
|
class Language(Enum):
|
|
|
|
|
C = 'C'
|
|
|
|
|
CPP = 'C++'
|
|
|
|
|
XML = 'XML'
|
|
|
|
|
ASCIIDOC = 'asciidoc'
|
|
|
|
|
JSON = 'JSON'
|
|
|
|
|
SH = 'sh'
|
|
|
|
|
|
|
|
|
|
UNKNOWN = 'UNKNOWN'
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.value
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def extension(self):
|
|
|
|
|
if self == Language.UNKNOWN:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"Can't get extension for UNKNOWN language")
|
|
|
|
|
if self == Language.ASCIIDOC:
|
|
|
|
|
return 'adoc'
|
|
|
|
|
return str(self).lower().replace('+', 'p')
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2019-07-29 08:09:30 -05:00
|
|
|
def from_string(cls, s):
|
2019-05-08 16:48:37 -05:00
|
|
|
s = s.upper()
|
|
|
|
|
for val in Language:
|
|
|
|
|
if s == str(val).upper():
|
|
|
|
|
return val
|
|
|
|
|
return Language.UNKNOWN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CodeExtractor(LinewiseFileProcessor):
|
2019-07-29 08:09:30 -05:00
|
|
|
def __init__(self, output_line_numbers=False, quiet=False):
|
2019-05-08 16:48:37 -05:00
|
|
|
super().__init__()
|
|
|
|
|
self.MIN_LINES = 5
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
self.output_line_numbers = output_line_numbers
|
2019-05-08 16:48:37 -05:00
|
|
|
self.quiet = quiet
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
self.next_snippet_id = 0
|
|
|
|
|
self.in_code_block = False
|
|
|
|
|
self.block_pattern = re.compile(r'\[source(,?)(?P<tags>.*)\]')
|
|
|
|
|
self.languages_to_extract = set((Language.CPP, Language.C))
|
|
|
|
|
self.code_lines = None
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
self.generated_files = []
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
# list of (generated file path, include path) pairs
|
|
|
|
|
self.deps = []
|
|
|
|
|
|
|
|
|
|
# key: generated file path
|
|
|
|
|
# value: line number where a code snippet starts
|
|
|
|
|
self.origins = {}
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def get_unique_id(self):
|
|
|
|
|
ret = self.next_snippet_id
|
|
|
|
|
self.next_snippet_id += 1
|
2019-05-08 16:48:37 -05:00
|
|
|
return ret
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def make_numbered_filename(self, language):
|
2019-05-08 16:48:37 -05:00
|
|
|
name = self.filename.with_suffix('.{num}.{ext}'.format(
|
2019-07-29 08:09:30 -05:00
|
|
|
num=self.get_unique_id(), ext=language.extension)).name
|
2019-05-08 16:48:37 -05:00
|
|
|
return GENCODEDIR / name
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def print_message(self, s):
|
2019-05-08 16:48:37 -05:00
|
|
|
if not self.quiet:
|
2019-07-29 08:09:30 -05:00
|
|
|
print('{}:{}: {}'.format(self.filename, self.line_number, s))
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def process_start_of_code_block(self):
|
|
|
|
|
prev_line = self.get_preceding_line()
|
|
|
|
|
if not prev_line:
|
2019-05-08 16:48:37 -05:00
|
|
|
# No previous line to find language.
|
|
|
|
|
return
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
code_block_tag = self.block_pattern.match(prev_line.rstrip())
|
|
|
|
|
if not code_block_tag:
|
2019-05-08 16:48:37 -05:00
|
|
|
# Not going to handle this.
|
|
|
|
|
return
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
tags = set(code_block_tag.group('tags').upper().split(','))
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
self.language = Language.UNKNOWN
|
|
|
|
|
for lang in Language:
|
|
|
|
|
if str(lang).upper() in tags:
|
|
|
|
|
self.language = lang
|
|
|
|
|
break
|
|
|
|
|
if self.language == Language.UNKNOWN:
|
2019-07-29 08:09:30 -05:00
|
|
|
self.print_message('Not extracting code snippet introduced with {} (tags = {})'.format(
|
|
|
|
|
code_block_tag.group(), tags))
|
2019-05-08 16:48:37 -05:00
|
|
|
return
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if self.language not in self.languages_to_extract:
|
|
|
|
|
self.print_message('Not extracting code snippet identified as {}'.format(
|
2019-05-08 16:48:37 -05:00
|
|
|
self.language))
|
|
|
|
|
return
|
|
|
|
|
if 'SUPPRESS-BUILD' in tags:
|
2019-07-29 08:09:30 -05:00
|
|
|
self.print_message(
|
2019-05-08 16:48:37 -05:00
|
|
|
'Suppressing extraction of code snippet because we saw "suppress-build"')
|
|
|
|
|
return
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
self.code_lines = []
|
|
|
|
|
self.start_of_code_block = self.line_number
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def process_end_of_code_block(self):
|
|
|
|
|
if self.code_lines is None:
|
|
|
|
|
return
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
code_lines = self.code_lines
|
|
|
|
|
self.code_lines = None
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if len(code_lines) < self.MIN_LINES:
|
|
|
|
|
self.print_message(
|
|
|
|
|
'Not extracting code snippet - only {} lines.'.format(len(code_lines)))
|
|
|
|
|
return
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
out_filename = self.make_numbered_filename(self.language)
|
|
|
|
|
self.print_message('Writing {} extracted lines to file {}\n'.format(
|
|
|
|
|
len(code_lines), out_filename.relative_to(Path('.').resolve())))
|
|
|
|
|
self.generated_files.append(out_filename)
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
self.origins[out_filename] = self.start_of_code_block
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
include_file = CODEDIR / out_filename.with_suffix('.h').name
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
with out_filename.open('w', encoding='utf-8') as f:
|
|
|
|
|
f.write('#include "common_include.h"\n')
|
|
|
|
|
if include_file.exists():
|
|
|
|
|
f.write('#include "{}"\n\n'.format(include_file.name))
|
|
|
|
|
self.deps.append((out_filename, include_file))
|
|
|
|
|
f.write('void func() {\n')
|
|
|
|
|
f.write(''.join(code_lines))
|
|
|
|
|
f.write('\n}\n')
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def process_code_block_line(self):
|
|
|
|
|
if self.code_lines is not None:
|
|
|
|
|
if self.output_line_numbers:
|
|
|
|
|
self.code_lines.append('# {} "{}"\n'.format(
|
|
|
|
|
self.line_number, self.filename))
|
|
|
|
|
self.code_lines.append(self.line)
|
|
|
|
|
|
|
|
|
|
def process_line(self, line_num, line):
|
2019-05-08 16:48:37 -05:00
|
|
|
if line.startswith('---'):
|
|
|
|
|
# Toggle code block status.
|
2019-07-29 08:09:30 -05:00
|
|
|
self.in_code_block = not self.in_code_block
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if self.in_code_block:
|
2019-05-08 16:48:37 -05:00
|
|
|
# We just started a code block
|
2019-07-29 08:09:30 -05:00
|
|
|
self.process_start_of_code_block()
|
2019-05-08 16:48:37 -05:00
|
|
|
else:
|
|
|
|
|
# We just ended one.
|
2019-07-29 08:09:30 -05:00
|
|
|
self.process_end_of_code_block()
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
elif self.in_code_block:
|
|
|
|
|
self.process_code_block_line()
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CodeExtractorGroup(object):
|
2019-07-29 08:09:30 -05:00
|
|
|
def __init__(self, output_line_numbers=False, quiet=False):
|
|
|
|
|
self.output_line_numbers = output_line_numbers
|
2019-05-08 16:48:37 -05:00
|
|
|
self.quiet = quiet
|
|
|
|
|
|
|
|
|
|
# key: adoc file path. value: list of generated source files.
|
2019-07-29 08:09:30 -05:00
|
|
|
self.generated_files = {}
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
# all generated sources files
|
2019-07-29 08:09:30 -05:00
|
|
|
self.all_generated = []
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
# list of (generated file path, include path) pairs
|
|
|
|
|
self.deps = []
|
|
|
|
|
|
|
|
|
|
# key: generated file path
|
|
|
|
|
# value: (adoc file path, line number where a code snippet starts) pair
|
|
|
|
|
self.origins = {}
|
|
|
|
|
|
|
|
|
|
def process(self, files):
|
|
|
|
|
for fn in files:
|
2019-07-29 08:09:30 -05:00
|
|
|
extractor = CodeExtractor(output_line_numbers=self.output_line_numbers,
|
2019-05-08 16:48:37 -05:00
|
|
|
quiet=self.quiet)
|
2019-07-29 08:09:30 -05:00
|
|
|
extractor.process_file(fn)
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if extractor.generated_files:
|
|
|
|
|
self.generated_files[extractor.filename] = extractor.generated_files
|
|
|
|
|
self.all_generated.extend(extractor.generated_files)
|
2019-05-08 16:48:37 -05:00
|
|
|
self.deps.extend(extractor.deps)
|
2019-07-29 08:09:30 -05:00
|
|
|
self.origins.update({fn: (extractor.filename, line_num)
|
|
|
|
|
for fn, line_num in extractor.origins.items()})
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
def output_makefile(self, makefile):
|
2019-05-08 16:48:37 -05:00
|
|
|
with open(makefile, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
|
|
generated_c_string = ' \\\n'.join(str(fn)
|
2019-07-29 08:09:30 -05:00
|
|
|
for fn in self.all_generated if fn.suffix == '.c')
|
2019-05-08 16:48:37 -05:00
|
|
|
generated_cpp_string = ' \\\n'.join(str(fn)
|
2019-07-29 08:09:30 -05:00
|
|
|
for fn in self.all_generated if fn.suffix == '.cpp')
|
2019-05-08 16:48:37 -05:00
|
|
|
deps_string = '\n'.join('{}: {} $(CODEDIR)/common_include.h'.format(fn.with_suffix('.o'), dep)
|
|
|
|
|
for fn, dep in self.deps)
|
|
|
|
|
extra_arg = ''
|
2019-07-29 08:09:30 -05:00
|
|
|
if self.output_line_numbers:
|
|
|
|
|
extra_arg = '--line_numbers'
|
2019-05-08 16:48:37 -05:00
|
|
|
f.write("""
|
|
|
|
|
OUTDIR ?= $(CURDIR)/{out}
|
|
|
|
|
CODEDIR ?= $(CURDIR)/{codedir}
|
|
|
|
|
PYTHON ?= python3
|
|
|
|
|
QUIET ?= @
|
|
|
|
|
|
|
|
|
|
GENERATED_C := {c}
|
|
|
|
|
C_OBJECTS := $(patsubst %.c,%.o,$(GENERATED_C))
|
|
|
|
|
|
|
|
|
|
GENERATED_CPP := {cpp}
|
|
|
|
|
CPP_OBJECTS := $(patsubst %.cpp,%.o,$(GENERATED_CPP))
|
|
|
|
|
|
|
|
|
|
build-examples: $(C_OBJECTS) $(CPP_OBJECTS)
|
|
|
|
|
.PHONY: build-examples
|
|
|
|
|
|
|
|
|
|
clean-examples:
|
|
|
|
|
\trm -f $(C_OBJECTS) $(CPP_OBJECTS)
|
|
|
|
|
.PHONY: clean-examples
|
|
|
|
|
|
|
|
|
|
$(C_OBJECTS) : %.o : %.c $(OUTDIR)/openxr/openxr.h
|
|
|
|
|
\t@echo '$(ORIGIN)'
|
|
|
|
|
\t$(QUIET)gcc -std=gnu99 -c -I$(OUTDIR) -I$(CODEDIR) $< -o $@
|
|
|
|
|
|
|
|
|
|
$(CPP_OBJECTS) : %.o : %.cpp $(OUTDIR)/openxr/openxr.h
|
|
|
|
|
\t@echo '$(ORIGIN)'
|
|
|
|
|
\t$(QUIET)g++ -std=gnu++11 -c -I$(OUTDIR) -I$(CODEDIR) $< -o $@
|
|
|
|
|
|
|
|
|
|
ifeq ($(strip $(QUIET)),@)
|
|
|
|
|
EXTRACT_QUIET := --quiet
|
|
|
|
|
endif
|
|
|
|
|
|
|
|
|
|
$(GENERATED_C) $(GENERATED_CPP) {makefile}: {script} {inputs}
|
|
|
|
|
\t$(QUIET)$(PYTHON) $< {extra} --makefile={makefile} $(EXTRACT_QUIET)
|
|
|
|
|
|
|
|
|
|
gen: {script}
|
|
|
|
|
\t$(QUIET)$(PYTHON) $< {extra} --makefile={makefile} $(EXTRACT_QUIET)
|
|
|
|
|
.PHONY: gen
|
|
|
|
|
|
|
|
|
|
{deps}
|
2019-07-29 08:09:30 -05:00
|
|
|
""".format(out=(ROOT / 'specification' / 'out' / '1.0').relative_to(Path('.').resolve()),
|
2019-05-08 16:48:37 -05:00
|
|
|
codedir=CODEDIR.relative_to(Path('.').resolve()),
|
|
|
|
|
c=generated_c_string,
|
|
|
|
|
cpp=generated_cpp_string,
|
|
|
|
|
makefile=makefile,
|
|
|
|
|
script=Path(__file__),
|
|
|
|
|
extra=extra_arg,
|
2019-08-02 19:20:10 -05:00
|
|
|
inputs=' '.join(str(infile)
|
|
|
|
|
for infile in self.generated_files),
|
2019-05-08 16:48:37 -05:00
|
|
|
deps=deps_string))
|
2019-07-29 08:09:30 -05:00
|
|
|
for fn, gen in self.generated_files.items():
|
2019-05-08 16:48:37 -05:00
|
|
|
f.write('{stem}: {files}\n.PHONY: {stem}\n'.format(
|
|
|
|
|
stem=fn.stem, files=' '.join(str(g.with_suffix('.o')) for g in gen)))
|
2019-08-02 19:20:10 -05:00
|
|
|
if self.origins:
|
|
|
|
|
width = max(len(generated.name) for generated in self.origins)
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-08-02 19:20:10 -05:00
|
|
|
for generated, origin in self.origins.items():
|
|
|
|
|
origin_file, origin_line = origin
|
|
|
|
|
if generated.suffix == '.cpp':
|
|
|
|
|
compiler = '[c++] '
|
|
|
|
|
else:
|
|
|
|
|
compiler = '[cc] '
|
|
|
|
|
origin_str = '{} {} extracted from {}:{}'.format(compiler, generated.name.ljust(width),
|
|
|
|
|
origin_file, origin_line)
|
|
|
|
|
f.write('{obj}: ORIGIN := {originstr}\n'.format(
|
|
|
|
|
obj=generated.with_suffix('.o'), originstr=origin_str))
|
2019-05-08 16:48:37 -05:00
|
|
|
|
|
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if __name__ == "__main__":
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
# if it already exists, that's OK
|
|
|
|
|
try:
|
|
|
|
|
GENCODEDIR.mkdir(parents=True)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
if e.errno != errno.EEXIST:
|
|
|
|
|
raise
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument("file",
|
|
|
|
|
help="Only extract from the indicated file(s). By default, all chapters and extensions are examined.",
|
|
|
|
|
nargs="*")
|
|
|
|
|
parser.add_argument("--line_numbers", "--linenumbers",
|
|
|
|
|
help='Add lines of the form "# line_number filename" to the output, for build errors/warnings that point to the adoc files.',
|
|
|
|
|
action='store_true')
|
|
|
|
|
parser.add_argument('--makefile',
|
|
|
|
|
help='Output a makefile with a build-examples target (and matching clean-examples target).',
|
|
|
|
|
type=str)
|
|
|
|
|
parser.add_argument('--quiet', '-q',
|
|
|
|
|
help="Don't output debug information about what we are extracting and not extracting.",
|
|
|
|
|
action='store_true')
|
|
|
|
|
# type=argparse.FileType('w', encoding='UTF-8'))
|
|
|
|
|
args = parser.parse_args()
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if args.file:
|
|
|
|
|
files = [Path(f).resolve() for f in args.file]
|
|
|
|
|
else:
|
|
|
|
|
files = ALL_DOCS
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
extractors = CodeExtractorGroup(output_line_numbers=args.line_numbers,
|
|
|
|
|
quiet=args.quiet)
|
|
|
|
|
extractors.process(files)
|
2019-05-08 16:48:37 -05:00
|
|
|
|
2019-07-29 08:09:30 -05:00
|
|
|
if args.makefile:
|
|
|
|
|
extractors.output_makefile(args.makefile)
|