Files

339 lines
12 KiB
Python
Raw Permalink Normal View History

#!/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
from spec_tools.file_process import LinewiseFileProcessor
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
def from_string(cls, s):
s = s.upper()
for val in Language:
if s == str(val).upper():
return val
return Language.UNKNOWN
class CodeExtractor(LinewiseFileProcessor):
def __init__(self, output_line_numbers=False, quiet=False):
super().__init__()
self.MIN_LINES = 5
self.output_line_numbers = output_line_numbers
self.quiet = quiet
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
self.generated_files = []
# list of (generated file path, include path) pairs
self.deps = []
# key: generated file path
# value: line number where a code snippet starts
self.origins = {}
def get_unique_id(self):
ret = self.next_snippet_id
self.next_snippet_id += 1
return ret
def make_numbered_filename(self, language):
name = self.filename.with_suffix('.{num}.{ext}'.format(
num=self.get_unique_id(), ext=language.extension)).name
return GENCODEDIR / name
def print_message(self, s):
if not self.quiet:
print('{}:{}: {}'.format(self.filename, self.line_number, s))
def process_start_of_code_block(self):
prev_line = self.get_preceding_line()
if not prev_line:
# No previous line to find language.
return
code_block_tag = self.block_pattern.match(prev_line.rstrip())
if not code_block_tag:
# Not going to handle this.
return
tags = set(code_block_tag.group('tags').upper().split(','))
self.language = Language.UNKNOWN
for lang in Language:
if str(lang).upper() in tags:
self.language = lang
break
if self.language == Language.UNKNOWN:
self.print_message('Not extracting code snippet introduced with {} (tags = {})'.format(
code_block_tag.group(), tags))
return
if self.language not in self.languages_to_extract:
self.print_message('Not extracting code snippet identified as {}'.format(
self.language))
return
if 'SUPPRESS-BUILD' in tags:
self.print_message(
'Suppressing extraction of code snippet because we saw "suppress-build"')
return
self.code_lines = []
self.start_of_code_block = self.line_number
def process_end_of_code_block(self):
if self.code_lines is None:
return
code_lines = self.code_lines
self.code_lines = None
if len(code_lines) < self.MIN_LINES:
self.print_message(
'Not extracting code snippet - only {} lines.'.format(len(code_lines)))
return
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)
self.origins[out_filename] = self.start_of_code_block
include_file = CODEDIR / out_filename.with_suffix('.h').name
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')
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):
if line.startswith('---'):
# Toggle code block status.
self.in_code_block = not self.in_code_block
if self.in_code_block:
# We just started a code block
self.process_start_of_code_block()
else:
# We just ended one.
self.process_end_of_code_block()
elif self.in_code_block:
self.process_code_block_line()
class CodeExtractorGroup(object):
def __init__(self, output_line_numbers=False, quiet=False):
self.output_line_numbers = output_line_numbers
self.quiet = quiet
# key: adoc file path. value: list of generated source files.
self.generated_files = {}
# all generated sources files
self.all_generated = []
# 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:
extractor = CodeExtractor(output_line_numbers=self.output_line_numbers,
quiet=self.quiet)
extractor.process_file(fn)
if extractor.generated_files:
self.generated_files[extractor.filename] = extractor.generated_files
self.all_generated.extend(extractor.generated_files)
self.deps.extend(extractor.deps)
self.origins.update({fn: (extractor.filename, line_num)
for fn, line_num in extractor.origins.items()})
def output_makefile(self, makefile):
with open(makefile, 'w', encoding='utf-8') as f:
generated_c_string = ' \\\n'.join(str(fn)
for fn in self.all_generated if fn.suffix == '.c')
generated_cpp_string = ' \\\n'.join(str(fn)
for fn in self.all_generated if fn.suffix == '.cpp')
deps_string = '\n'.join('{}: {} $(CODEDIR)/common_include.h'.format(fn.with_suffix('.o'), dep)
for fn, dep in self.deps)
extra_arg = ''
if self.output_line_numbers:
extra_arg = '--line_numbers'
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}
""".format(out=(ROOT / 'specification' / 'out' / '1.0').relative_to(Path('.').resolve()),
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),
deps=deps_string))
for fn, gen in self.generated_files.items():
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-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))
if __name__ == "__main__":
# 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()
if args.file:
files = [Path(f).resolve() for f in args.file]
else:
files = ALL_DOCS
extractors = CodeExtractorGroup(output_line_numbers=args.line_numbers,
quiet=args.quiet)
extractors.process(files)
if args.makefile:
extractors.output_makefile(args.makefile)