diff options
-rw-r--r-- | setup.cfg | 3 | ||||
-rw-r--r-- | yapymake/__init__.py | 3 | ||||
-rw-r--r-- | yapymake/args.py | 23 | ||||
-rw-r--r-- | yapymake/makefile/__init__.py | 92 | ||||
-rw-r--r-- | yapymake/makefile/parse_util.py | 64 | ||||
-rw-r--r-- | yapymake/makefile/token.py | 40 | ||||
-rw-r--r-- | yapymake/util/__init__.py | 4 |
7 files changed, 131 insertions, 98 deletions
@@ -26,3 +26,6 @@ where = . [options.entry_points] console_scripts = yapymake = yapymake:main + +[mypy] +strict = True diff --git a/yapymake/__init__.py b/yapymake/__init__.py index fe4b182..8a0c31d 100644 --- a/yapymake/__init__.py +++ b/yapymake/__init__.py @@ -4,7 +4,7 @@ DESCRIPTION = 'A (mostly) POSIX-compatible make implemented in Python' from .args import parse from .makefile import Makefile -def main(): +def main() -> None: these_args = parse() file = Makefile(these_args) # TODO dump command line into MAKEFLAGS @@ -15,6 +15,7 @@ def main(): targets = [arg for arg in these_args.targets_or_macros if '=' not in arg] if len(targets) == 0: + assert file.first_non_special_target is not None targets = [file.first_non_special_target] for target in targets: diff --git a/yapymake/args.py b/yapymake/args.py index 2252855..3f9d842 100644 --- a/yapymake/args.py +++ b/yapymake/args.py @@ -1,9 +1,8 @@ import argparse from dataclasses import dataclass -import io import os import sys -from typing import List, TextIO +from typing import List, Optional, TextIO from . import DESCRIPTION, VERSION @@ -95,21 +94,25 @@ class Args: self.touch = parsed_args.touch self.targets_or_macros = parsed_args.targets_or_macros -def parse(cli_args: List[str] = None, env_makeflags: str = None) -> Args: +def parse(cli_args: Optional[List[str]] = None, env_makeflags: Optional[str] = None) -> Args: if cli_args is None: - cli_args = sys.argv[1:] + real_cli_args = sys.argv[1:] + else: + real_cli_args = cli_args if env_makeflags is None: - env_makeflags = os.environ.get('MAKEFLAGS', '') + real_env_makeflags = os.environ.get('MAKEFLAGS', '') + else: + real_env_makeflags = env_makeflags # per POSIX, we accept option letters without a leading -, so to simplify we prepend a - now # TODO allow macro definitions in MAKEFLAGS - if len(env_makeflags) > 0 and not env_makeflags.startswith('-'): - env_makeflags = '-' + env_makeflags + if len(real_env_makeflags) > 0 and not real_env_makeflags.startswith('-'): + real_env_makeflags = '-' + real_env_makeflags - if len(env_makeflags) > 0: - all_args = [env_makeflags, *cli_args] + if len(real_env_makeflags) > 0: + all_args = [real_env_makeflags, *real_cli_args] else: - all_args = cli_args + all_args = real_cli_args return Args(parser.parse_args(all_args)) diff --git a/yapymake/makefile/__init__.py b/yapymake/makefile/__init__.py index 7a2adfe..70b61d8 100644 --- a/yapymake/makefile/__init__.py +++ b/yapymake/makefile/__init__.py @@ -32,7 +32,8 @@ class Makefile: if args.builtin_rules: self._inference_rules += BUILTIN_INFERENCE_RULES - self._macros.update(BUILTIN_MACROS) + for k, v in BUILTIN_MACROS.items(): + self._macros[k] = (MacroSource.Builtin, TokenString.text(v)) for target in BUILTIN_TARGETS: self._targets[target.name] = target @@ -47,7 +48,7 @@ class Makefile: # TODO either discern command line vs MAKEFLAGS or don't pretend we can self._macros[name] = (MacroSource.CommandLine, TokenString.text(value)) - def read(self, file: TextIO): + def read(self, file: TextIO) -> None: lines_iter: PeekableIterator[str] = PeekableIterator(iter(file)) for line in lines_iter: # handle escaped newlines (POSIX says these are different in command lines (which we handle later) and @@ -101,21 +102,23 @@ class Makefile: if line_type == 'rule': # > Target entries are specified by a <blank>-separated, non-null list of targets, then a <colon>, then # > a <blank>-separated, possibly empty list of prerequisites. - targets, after_colon = line_tokens.split_once(':') - targets = self.expand_macros(targets).split() + colon_split = line_tokens.split_once(':') + assert colon_split is not None + targets_tokens, after_colon = colon_split + targets = self.expand_macros(targets_tokens).split() # > Text following a <semicolon>, if any, and all following lines that begin with a <tab>, are makefile # > command lines to be executed to update the target. semicolon_split = after_colon.split_once(';') if semicolon_split is None: prerequisites = self.expand_macros(after_colon).split() - commands = [] + command_token_strings = [] else: - prerequisites, commands = semicolon_split - prerequisites = self.expand_macros(prerequisites).split() - commands = [commands] + prerequisite_tokens, command_tokens = semicolon_split + prerequisites = self.expand_macros(prerequisite_tokens).split() + command_token_strings = [command_tokens] while lines_iter.peek().startswith('\t'): - commands.append(tokenize(next(lines_iter).lstrip('\t'))) - commands = [CommandLine(c) for c in commands] + command_token_strings.append(tokenize(next(lines_iter).lstrip('\t'))) + commands = [CommandLine(c) for c in command_token_strings] # we don't know yet if it's a target rule or an inference rule match = re.fullmatch(r'(?P<s1>(\.[^/.]+)?)(?P<s2>\.[^/.]+)', targets[0]) @@ -139,17 +142,19 @@ class Makefile: elif line_type == 'macro': # > The macro named string1 is defined as having the value of string2, where string2 is defined as all # > characters, if any, after the <equals-sign>... - name, value = line_tokens.split_once('=') + equals_split = line_tokens.split_once('=') + assert equals_split is not None + name_tokens, value = equals_split # > up to a comment character ( '#' ) or an unescaped <newline>. comment_split = value.split_once('#') if comment_split is not None: value, _ = comment_split # > Any <blank> characters immediately before or after the <equals-sign> shall be ignored. - name.rstrip() + name_tokens.rstrip() value.lstrip() # > Macros in the string before the <equals-sign> in a macro definition shall be evaluated when the # > macro assignment is made. - name = self.expand_macros(name) + name = self.expand_macros(name_tokens) # > Macros defined in the makefile(s) shall override macro definitions that occur before them in the # > makefile(s) and macro definitions from source 4. If the -e option is not specified, macros defined # > in the makefile(s) shall override macro definitions from source 3. Macros defined in the makefile(s) @@ -172,47 +177,50 @@ class Makefile: internal_macro = len(macro_name) in [1, 2] and macro_name[0] in '@?<*' and \ macro_name[1:] in ['', 'D', 'F'] if internal_macro: + assert current_target is not None if macro_name[0] == '@': # > The $@ shall evaluate to the full target name of the current target, or the archive filename # > part of a library archive target. It shall be evaluated for both target and inference rules. - macro_value = [current_target.name] + macro_pieces = [current_target.name] elif macro_name[0] == '?': # > The $? macro shall evaluate to the list of prerequisites that are newer than the current # > target. It shall be evaluated for both target and inference rules. - macro_value = [p for p in current_target.prerequisites if self.target(p).newer_than(current_target)] + macro_pieces = [p for p in current_target.prerequisites if self.target(p).newer_than(current_target)] elif macro_name[0] == '<': # > In an inference rule, the $< macro shall evaluate to the filename whose existence allowed # > the inference rule to be chosen for the target. In the .DEFAULT rule, the $< macro shall # > evaluate to the current target name. - macro_value = current_target.prerequisites + macro_pieces = current_target.prerequisites elif macro_name[0] == '*': # > The $* macro shall evaluate to the current target name with its suffix deleted. - macro_value = [str(PurePath(current_target.name).with_suffix(''))] + macro_pieces = [str(PurePath(current_target.name).with_suffix(''))] else: # this shouldn't happen - macro_value = [] + macro_pieces = [] if macro_name[1:] == 'D': - macro_value = [str(PurePath(x).parent) for x in macro_value] + macro_pieces = [str(PurePath(x).parent) for x in macro_pieces] elif macro_name[1:] == 'F': - macro_value = [str(PurePath(x).name) for x in macro_value] + macro_pieces = [str(PurePath(x).name) for x in macro_pieces] - macro_value = TokenString.text(' '.join(macro_value)) + macro_tokens = TokenString.text(' '.join(macro_pieces)) else: - _, macro_value = self._macros[this_token.name] - macro_value = self.expand_macros(macro_value, current_target) + _, macro_tokens = self._macros[this_token.name] + macro_value = self.expand_macros(macro_tokens, current_target) if this_token.replacement is not None: replaced, replacement = (self.expand_macros(t, current_target) for t in this_token.replacement) macro_value = re.sub(re.escape(replaced) + r'\b', replacement, macro_value) return macro_value + else: + raise TypeError('unexpected token type!') return ''.join(expand_one(t) for t in text) def special_target(self, name: str) -> Optional['Target']: return self._targets.get(name, None) - def special_target_has_prereq(self, target: str, name: str) -> bool: - target = self.special_target(target) + def special_target_has_prereq(self, target_name: str, name: str) -> bool: + target = self.special_target(target_name) if target is None: return False return len(target.prerequisites) == 0 or name in target.prerequisites @@ -269,6 +277,8 @@ class Target: path = self._path() if path.exists(): return path.stat().st_mtime + else: + return None def newer_than(self, other: 'Target') -> Optional[bool]: self_mtime = self.modified_time() @@ -279,6 +289,8 @@ class Target: return True elif other_mtime is None and other.already_updated and other.name in self.prerequisites: return False + else: + return None def is_up_to_date(self, file: Makefile) -> bool: if self.already_updated: @@ -287,14 +299,14 @@ class Target: newer_than_all_dependencies = all(self.newer_than(file.target(other)) for other in self.prerequisites) return exists and newer_than_all_dependencies - def update(self, file: Makefile): + def update(self, file: Makefile) -> None: for prerequisite in self.prerequisites: file.target(prerequisite).update(file) if not self.is_up_to_date(file): self.execute_commands(file) self.already_updated = True - def execute_commands(self, file: Makefile): + def execute_commands(self, file: Makefile) -> None: for command in self.commands: command.execute(file, self) @@ -333,7 +345,7 @@ class CommandLine: first_token.text = first_token.text[1:] self.execution_line = TokenString(list((first_token, *tokens_iter))) - def execute(self, file: Makefile, current_target: 'Target'): + def execute(self, file: Makefile, current_target: 'Target') -> None: # POSIX: # > If the command prefix contains a <hyphen-minus>, or the -i option is present, or the special target .IGNORE # > has either the current target as a prerequisite or has no prerequisites, any error found while executing @@ -415,18 +427,18 @@ BUILTIN_INFERENCE_RULES = [ ]), ] BUILTIN_MACROS = { - 'MAKE': TokenString.text('make'), - 'AR': TokenString.text('ar'), - 'ARFLAGS': TokenString.text('-rv'), - 'YACC': TokenString.text('yacc'), - 'YFLAGS': TokenString.text(''), - 'LEX': TokenString.text('lex'), - 'LFLAGS': TokenString.text(''), - 'LDFLAGS': TokenString.text(''), - 'CC': TokenString.text('c99'), - 'CFLAGS': TokenString.text('-O 1'), - 'FC': TokenString.text('fort77'), - 'FFLAGS': TokenString.text('-O 1'), + 'MAKE': 'make', + 'AR': 'ar', + 'ARFLAGS': '-rv', + 'YACC': 'yacc', + 'YFLAGS': '', + 'LEX': 'lex', + 'LFLAGS': '', + 'LDFLAGS': '', + 'CC': 'c99', + 'CFLAGS': '-O 1', + 'FC': 'fort77', + 'FFLAGS': '-O 1', } BUILTIN_TARGETS = [ Target('.SUFFIXES', ['.o', '.c', '.y', '.l', '.a', '.sh', '.f'], []), diff --git a/yapymake/makefile/parse_util.py b/yapymake/makefile/parse_util.py index ad1e363..ded7476 100644 --- a/yapymake/makefile/parse_util.py +++ b/yapymake/makefile/parse_util.py @@ -43,6 +43,7 @@ def tag(tag_text: str) -> Parser[None]: def parse(text: str) -> ParseResult[None]: if text.startswith(tag_text): return None, text[len(tag_text):] + return None return parse def take_while1(predicate: Callable[[str], bool]) -> Parser[str]: @@ -61,13 +62,14 @@ def take_till1(predicate: Callable[[str], bool]) -> Parser[str]: def any_char(text: str) -> ParseResult[str]: if len(text) > 0: return text[0], text[1:] + return None def all_consuming(parser: Parser[T]) -> Parser[T]: def parse(text: str) -> ParseResult[T]: - result = parser(text) - if result is None: + parsed_result = parser(text) + if parsed_result is None: return None - result, extra = result + result, extra = parsed_result if len(extra) > 0: return None return result, '' @@ -75,10 +77,10 @@ def all_consuming(parser: Parser[T]) -> Parser[T]: def map_parser(parser: Parser[T1], mapper: Callable[[T1], T2]) -> Parser[T2]: def parse(text: str) -> ParseResult[T2]: - result = parser(text) - if result is None: + parsed_result = parser(text) + if parsed_result is None: return None - result, extra = result + result, extra = parsed_result return mapper(result), extra return parse @@ -92,10 +94,10 @@ def opt(parser: Parser[T]) -> Parser[Optional[T]]: def verify(parser: Parser[T], predicate: Callable[[T], bool]) -> Parser[T]: def parse(text: str) -> ParseResult[T]: - result = parser(text) - if result is None: + parsed_result = parser(text) + if parsed_result is None: return None - result, extra = result + result, extra = parsed_result if predicate(result): return result, extra return None @@ -106,13 +108,13 @@ def many1(parser: Parser[T]) -> Parser[List[T]]: parser_result = parser(text) if parser_result is None: return None - parser_result, extra = parser_result - result = [parser_result] + this_result, extra = parser_result + result = [this_result] parser_result = parser(extra) while parser_result is not None: - parser_result, extra = parser_result - result.append(parser_result) + this_result, extra = parser_result + result.append(this_result) parser_result = parser(extra) return result, extra return parse @@ -124,10 +126,10 @@ def delimited(before_parser: Parser[T1], parser: Parser[T], after_parser: Parser return None _, extra = before_result - result = parser(extra) - if result is None: + parsed_result = parser(extra) + if parsed_result is None: return None - result, extra = result + result, extra = parsed_result after_result = after_parser(extra) if after_result is None: @@ -139,15 +141,15 @@ def delimited(before_parser: Parser[T1], parser: Parser[T], after_parser: Parser def pair(first_parser: Parser[T1], second_parser: Parser[T2]) -> Parser[Tuple[T1, T2]]: def parse(text: str) -> ParseResult[Tuple[T1, T2]]: - first_result = first_parser(text) - if first_result is None: + first_parsed_result = first_parser(text) + if first_parsed_result is None: return None - first_result, extra = first_result + first_result, extra = first_parsed_result - second_result = second_parser(extra) - if second_result is None: + second_parsed_result = second_parser(extra) + if second_parsed_result is None: return None - second_result, extra = second_result + second_result, extra = second_parsed_result return (first_result, second_result), extra return parse @@ -159,30 +161,30 @@ def preceded(before_parser: Parser[T1], parser: Parser[T]) -> Parser[T]: return None _, extra = before_result - result = parser(extra) - if result is None: + parsed_result = parser(extra) + if parsed_result is None: return None - result, extra = result + result, extra = parsed_result return result, extra return parse def separated_pair(first_parser: Parser[T1], between_parser: Parser[T], second_parser: Parser[T2]) -> Parser[Tuple[T1, T2]]: def parse(text: str) -> ParseResult[Tuple[T1, T2]]: - first_result = first_parser(text) - if first_result is None: + first_parsed_result = first_parser(text) + if first_parsed_result is None: return None - first_result, extra = first_result + first_result, extra = first_parsed_result between_result = between_parser(extra) if between_result is None: return None _, extra = between_result - second_result = second_parser(extra) - if second_result is None: + second_parsed_result = second_parser(extra) + if second_parsed_result is None: return None - second_result, extra = second_result + second_result, extra = second_parsed_result return (first_result, second_result), extra return parse diff --git a/yapymake/makefile/token.py b/yapymake/makefile/token.py index 948fcac..fd7de5b 100644 --- a/yapymake/makefile/token.py +++ b/yapymake/makefile/token.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Iterable, Iterator, List, Optional, Tuple +from typing import Iterable, Iterator, List, MutableSequence, Optional, Tuple from .parse_util import * @@ -12,18 +12,19 @@ __all__ = [ ] class TokenString(Iterable['Token']): - _tokens: List['Token'] + _tokens: MutableSequence['Token'] - def __init__(self, my_tokens: List['Token'] = None): + def __init__(self, my_tokens: Optional[MutableSequence['Token']] = None): if my_tokens is None: - my_tokens = [] - self._tokens = my_tokens + self._tokens = [] + else: + self._tokens = my_tokens @staticmethod def text(body: str) -> 'TokenString': return TokenString([TextToken(body)]) - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: return isinstance(other, TokenString) and self._tokens == other._tokens def __iter__(self) -> Iterator['Token']: @@ -33,7 +34,7 @@ class TokenString(Iterable['Token']): return f'TokenString({repr(self._tokens)})' def split_once(self, delimiter: str) -> Optional[Tuple['TokenString', 'TokenString']]: - result0 = [] + result0: List[Token] = [] self_iter = iter(self._tokens) for t in self_iter: if isinstance(t, TextToken) and delimiter in t.text: @@ -44,13 +45,13 @@ class TokenString(Iterable['Token']): result0.append(t) return None - def lstrip(self): + def lstrip(self) -> None: first_token = self._tokens[0] if isinstance(first_token, TextToken): first_token.text = first_token.text.lstrip() self._tokens[0] = first_token - def rstrip(self): + def rstrip(self) -> None: last_token = self._tokens[-1] if isinstance(last_token, TextToken): last_token.text = last_token.text.rstrip() @@ -67,13 +68,18 @@ class TextToken(Token): @dataclass() class MacroToken(Token): name: str - replacement: Optional[Tuple[TokenString, TokenString]] = None + replacement: Optional[Tuple[TokenString, TokenString]] macro_name = take_while1(lambda c: c.isalnum() or c in ['.', '_']) def macro_expansion_body(end: str) -> Parser[MacroToken]: - subst = preceded(tag(":"), separated_pair(tokens('='), '=', tokens(end))) - return map_parser(pair(macro_name, opt(subst)), MacroToken) + subst = preceded(tag(":"), separated_pair(tokens('='), tag('='), tokens(end))) + + def make_token(data: Tuple[str, Optional[Tuple[TokenString, TokenString]]]) -> MacroToken: + name, replacement = data + return MacroToken(name, replacement) + + return map_parser(pair(macro_name, opt(subst)), make_token) def parens_macro_expansion(text: str) -> ParseResult[MacroToken]: return delimited(tag('$('), macro_expansion_body(')'), tag(')'))(text) @@ -85,12 +91,12 @@ def build_tiny_expansion(name_probably: str) -> Token: if name_probably == '$': return TextToken('$') else: - return MacroToken(name_probably) + return MacroToken(name_probably, None) -def tiny_macro_expansion(text: str) -> ParseResult[MacroToken]: +def tiny_macro_expansion(text: str) -> ParseResult[Token]: return map_parser(preceded(tag('$'), verify(any_char, lambda c: c not in ['(', '{'])), build_tiny_expansion)(text) -def macro_expansion(text: str) -> ParseResult[MacroToken]: +def macro_expansion(text: str) -> ParseResult[Token]: return alt(tiny_macro_expansion, parens_macro_expansion, braces_macro_expansion)(text) just_text = map_parser(take_till1(lambda c: c == '$'), TextToken) @@ -114,7 +120,9 @@ def full_text_tokens(text: str) -> ParseResult[TokenString]: return all_consuming(tokens())(text) def tokenize(text: str) -> TokenString: - result, _ = full_text_tokens(text) + parsed_result = full_text_tokens(text) + assert parsed_result is not None + result, _ = parsed_result return result # TODO handle errors diff --git a/yapymake/util/__init__.py b/yapymake/util/__init__.py index b26bbcd..04399fd 100644 --- a/yapymake/util/__init__.py +++ b/yapymake/util/__init__.py @@ -1 +1,5 @@ +__all__ = [ + 'PeekableIterator', +] + from .peekable_iterator import PeekableIterator |