Source code for efrotools.makefile
# Released under the MIT License. See LICENSE for details.
#
"""Tools for parsing/filtering makefiles."""
from __future__ import annotations
import copy
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
[docs]
@dataclass
class Section:
"""Represents a section of a Makefile."""
name: str | None
paragraphs: list[Paragraph]
[docs]
@dataclass
class Paragraph:
"""Represents a continuous set of non-blank lines in a Makefile."""
contents: str
[docs]
def get_logical_lines(self) -> list[str]:
"""Return contents broken into logical lines.
Lines joined by continuation chars are considered a single line.
"""
return self.contents.replace('\\\n', '').splitlines()
[docs]
class Makefile:
"""Represents an entire Makefile."""
header_line_full = '#' * 80
header_line_empty = '#' + ' ' * 78 + '#'
def __init__(self, contents: str):
self.sections: list[Section] = []
self._original = copy.copy(contents)
lines = contents.splitlines()
paragraphs: list[Paragraph] = []
# First off, break into paragraphs (continuous sets of lines)
plines: list[str] = []
for line in lines:
if line.strip() == '':
if plines:
paragraphs.append(Paragraph(contents='\n'.join(plines)))
plines = []
continue
plines.append(line)
if plines:
paragraphs.append(Paragraph(contents='\n'.join(plines)))
# Now break all paragraphs into sections.
section = Section(name=None, paragraphs=[])
self.sections.append(section)
for paragraph in paragraphs:
# Look for our very particular section headers and start
# a new section whenever we come across one.
plines = paragraph.contents.splitlines()
# pylint: disable=too-many-boolean-expressions
if (
len(plines) == 5
and plines[0] == self.header_line_full
and plines[1] == self.header_line_empty
and len(plines[2]) == 80
and plines[2][0] == '#'
and plines[2][-1] == '#'
and plines[3] == self.header_line_empty
and plines[4] == self.header_line_full
):
section = Section(name=plines[2][1:-1].strip(), paragraphs=[])
self.sections.append(section)
else:
section.paragraphs.append(paragraph)
[docs]
def find_assigns(self, name: str) -> list[tuple[Section, int]]:
"""Return section/index pairs for paragraphs containing an assign.
Note that the paragraph may contain other statements as well.
"""
found: list[tuple[Section, int]] = []
for section in self.sections:
for i, paragraph in enumerate(section.paragraphs):
if any(
line.split('=')[0].strip() == name
for line in paragraph.get_logical_lines()
):
found.append((section, i))
return found
[docs]
def find_targets(self, name: str) -> list[tuple[Section, int]]:
"""Return section/index pairs for paragraphs containing a target.
Note that the paragraph may contain other statements as well.
"""
found: list[tuple[Section, int]] = []
for section in self.sections:
for i, paragraph in enumerate(section.paragraphs):
if any(
line.split()[0] == name + ':'
for line in paragraph.get_logical_lines()
):
found.append((section, i))
return found
[docs]
def get_output(self) -> str:
"""Generate a Makefile from the current state."""
output = ''
for section in self.sections:
did_first_entry = False
if section.name is not None:
output += '\n\n' + self.header_line_full + '\n'
output += self.header_line_empty + '\n'
spacelen = 78 - len(section.name)
output += '#' + ' ' * (spacelen // 2) + section.name
spacelen -= spacelen // 2
output += ' ' * spacelen + '#\n'
output += self.header_line_empty + '\n'
output += self.header_line_full + '\n'
did_first_entry = True
for paragraph in section.paragraphs:
if did_first_entry:
output += '\n'
output += paragraph.contents + '\n'
did_first_entry = True
# print(output)
return output
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum