├── log └── .gitkeep ├── pymake ├── __init__.py ├── __main__.py ├── hello.c ├── todo.py ├── debug.py ├── version.py ├── flatten.py ├── printable.py ├── html.py ├── submake.py ├── makedb.py ├── scanner.py ├── hexdump.py ├── source.py ├── functions_cond.py ├── constants.py ├── wildcard.py └── error.py ├── tests ├── smallest.mk ├── varref.mk ├── test_undefine.py ├── envrules.mk ├── requirements.txt ├── warn-undefined.mk ├── hello.c ├── iampython.mk ├── test_target_specific_assign.mk ├── warning.mk ├── path.mk ├── abspath.mk ├── recipe-prefix.mk ├── comma.mk ├── direct_indirect.mk ├── dollar.mk ├── range.mk ├── basename.mk ├── addsuffix.mk ├── ignoreandsilent.mk ├── test_empty_variable_name.py ├── lastword.mk ├── test_varname.py ├── override.mk ├── recursive.mk ├── inpath.mk ├── confusing-directive.mk ├── flavor.mk ├── word.mk ├── envvar.mk ├── rule_without_target.mk ├── makefile_list.mk ├── counted.mk ├── mkseq.mk ├── addprefix.mk ├── dir.mk ├── if.mk ├── sort.mk ├── notdir.mk ├── functions_fs.mk ├── file.mk ├── automatic.mk ├── built-ins.mk ├── variables.mk ├── words.mk ├── space.mk ├── realpath.mk ├── suffix.mk ├── submake.mk ├── verify.py ├── vpath.mk ├── hello.mk ├── deref.mk ├── tabs.mk ├── strip.mk ├── test_shellflags.py ├── value.mk ├── rule-specific-var.mk ├── join.mk ├── wildcard.mk ├── env_recursion.mk ├── target-specific.mk ├── shellstatus.mk ├── functions.mk ├── call.mk ├── assign.mk ├── pattern.mk ├── lispy.mk ├── test_eval.py ├── firstword.mk ├── test_builtins.py ├── filepatsubst.mk ├── origin.mk ├── eval.mk ├── dot-posix.mk ├── test_define.mk ├── test_assign.py ├── undefine.mk ├── ifeq-nested.mk ├── whitespace.mk ├── test_warnings.py ├── append.mk ├── ifdef-recipe.mk ├── info.mk ├── test_comment.py ├── test_vchar.py ├── comments.mk ├── test_functions.py ├── conditional.mk ├── test_iter.py ├── balanced-parens.mk ├── test_whitespace.py ├── wordlist.mk ├── multiline.mk ├── test_sexpr.py ├── shell.mk ├── test_scanner.py ├── include.mk ├── findstring.mk ├── functions_str.mk ├── test_ifdef.py ├── export.mk ├── everything.mk ├── filter.mk ├── filter-out.mk ├── math.mk ├── directive.mk ├── subst.mk ├── test_env_recurse.py ├── foreach.mk ├── ifeq.mk ├── test_origin.py ├── lhs.mk ├── test_args.py ├── recipe.mk ├── define.mk ├── test_recipes.py ├── test_include.py ├── test_wildcard.py ├── test_seek_word.py ├── test_recipe_prefix.py ├── test_shell.py └── test_ifeq.py ├── docs ├── make-4.3.pdf └── make-4.4.1.pdf ├── .gitignore ├── examples ├── gnu_make.py ├── statement.py ├── backslash.py ├── showtokens.py ├── virtline.py ├── parse.py ├── tokeniz.py ├── rules.py └── conditional.py ├── NEWS ├── .github └── workflows │ └── build.yml ├── setup.py ├── notes.md └── tox.ini /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pymake/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/smallest.mk: -------------------------------------------------------------------------------- 1 | @:;@: 2 | -------------------------------------------------------------------------------- /tests/varref.mk: -------------------------------------------------------------------------------- 1 | b=42 2 | a=$b 3 | a=2$b2 4 | 5 | -------------------------------------------------------------------------------- /pymake/__main__.py: -------------------------------------------------------------------------------- 1 | from pymake import pymake 2 | 3 | pymake.main() 4 | 5 | -------------------------------------------------------------------------------- /docs/make-4.3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxlizard/pymake/HEAD/docs/make-4.3.pdf -------------------------------------------------------------------------------- /docs/make-4.4.1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxlizard/pymake/HEAD/docs/make-4.4.1.pdf -------------------------------------------------------------------------------- /tests/test_undefine.py: -------------------------------------------------------------------------------- 1 | import run 2 | 3 | def test1(): 4 | makefile = """ 5 | """ 6 | -------------------------------------------------------------------------------- /tests/envrules.mk: -------------------------------------------------------------------------------- 1 | # run with 2 | # a="foo:bar" b="bar:;echo bar" make -f envrules.mk 3 | $a 4 | $b 5 | 6 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest >= 3.6.0 2 | pytest-xdist 3 | pytest-timeout 4 | pytest-cov 5 | coverage 6 | -------------------------------------------------------------------------------- /pymake/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main( void ) 4 | { 5 | printf("hello, world!\n"); 6 | return 0; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /tests/warn-undefined.mk: -------------------------------------------------------------------------------- 1 | # --warn-undefined-variables Warn when an undefined variable is referenced. 2 | # 3 | 4 | $(info FOO=$(FOO)) 5 | 6 | @:;@: 7 | 8 | -------------------------------------------------------------------------------- /tests/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(void) 5 | { 6 | printf("Hello, world\n"); 7 | return EXIT_SUCCESS; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/iampython.mk: -------------------------------------------------------------------------------- 1 | 2 | SHELL="/bin/echo" 3 | 4 | if sys.version_info.major < 3: 5 | raise Exception("Requires Python 3.x") 6 | 7 | #% : ; @echo {implicit} $@ 8 | 9 | -------------------------------------------------------------------------------- /tests/test_target_specific_assign.mk: -------------------------------------------------------------------------------- 1 | 2 | hello: hello.o 3 | 4 | hello.o: CFLAGS+=-DWORLD="\"WoRlD\"" 5 | hello.o: hello.c hello.h 6 | 7 | clean: ; $(RM) hello hello.o 8 | -------------------------------------------------------------------------------- /tests/warning.mk: -------------------------------------------------------------------------------- 1 | $(warning hello, world) 2 | 3 | $(warning Hello, world)$(warning Hello, World) 4 | 5 | q$(warning I am q)=42 6 | $(warning q=$q) 7 | 8 | all:;@: 9 | 10 | 11 | -------------------------------------------------------------------------------- /pymake/todo.py: -------------------------------------------------------------------------------- 1 | # usage: 2 | # class SomeClass(TODOMixIn, RealSuperClass): 3 | # 4 | class TODOMixIn: 5 | def __init__(self, *na): 6 | raise NotImplementedError(self.name) 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/path.mk: -------------------------------------------------------------------------------- 1 | $(info $(PATH)) 2 | path=$(PATH) 3 | $(info path=$(path)) 4 | 5 | path=$(subst :, ,$(PATH)) 6 | $(info $(path)) 7 | # 8 | #$(info $(notdir $(path))) 9 | #$(info $(dir $(path))) 10 | 11 | @:;@: 12 | -------------------------------------------------------------------------------- /tests/abspath.mk: -------------------------------------------------------------------------------- 1 | 2 | $(info $(abspath abspath.mk)) 3 | 4 | a=abspath.mk 5 | $(info $(abspath $a)) 6 | $(info $(abspath $a $a)) 7 | 8 | $(info $(abspath $(wordlist 1,3,$(sort $(wildcard *.py))))) 9 | 10 | @:;@: 11 | 12 | -------------------------------------------------------------------------------- /tests/recipe-prefix.mk: -------------------------------------------------------------------------------- 1 | # Tinker with .RECIPEPREFIX 2 | # Note: only supported after Make 3.81 3 | # davep 24-Sep-2014 4 | 5 | .RECIPEPREFIX=q 6 | 7 | all : ; @echo = all=$@ 8 | 9 | foo : 10 | q@echo = foo=$@ 11 | 12 | -------------------------------------------------------------------------------- /tests/comma.mk: -------------------------------------------------------------------------------- 1 | # from the GNU make manual 2 | comma:= , 3 | empty:= 4 | space:= $(empty) $(empty) 5 | foo:= a b c 6 | bar:= $(subst $(space),$(comma),$(foo)) 7 | 8 | # bar is now ‘a,b,c’. 9 | $(info bar is now $(bar)) 10 | 11 | @:;@: 12 | -------------------------------------------------------------------------------- /tests/direct_indirect.mk: -------------------------------------------------------------------------------- 1 | # direct and indirect assignment 2 | path:=$(PATH) 3 | split_path=$(subst :, ,$(path)) 4 | $(info $(split_path)) 5 | path=foo:bar:baz: 6 | $(info $(split_path)) 7 | path=a:b:c 8 | $(info $(split_path)) 9 | 10 | @:;@: 11 | 12 | -------------------------------------------------------------------------------- /tests/dollar.mk: -------------------------------------------------------------------------------- 1 | # in make, a $$ -> literal $ 2 | # 3 | a=q 4 | 5 | @: q 6 | echo $ a 7 | 8 | $(a) : 9 | echo a=$a 10 | b=$a && echo $$$$ 11 | b=$$a a=10 && echo da=$a dda=$$a ddda=$$$a dddda=$$$$a dddddda=$$$$$$$a ddddddda=$$$$$$$a 12 | 13 | -------------------------------------------------------------------------------- /tests/range.mk: -------------------------------------------------------------------------------- 1 | # create a list of numbers starting from 0 2 | # $1 - number of elements in the list 3 | # e.g., $(call range,10) -> 0 1 2 3 4 5 6 7 8 9 4 | define range 5 | $(if $(word $(1),$(2)),$(2),$(call range,$(1),$(2) $(words $(2)))) 6 | endef 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/basename.mk: -------------------------------------------------------------------------------- 1 | $(info b=$(basename src/foo.c src-1.0/bar qux.c hacks)) 2 | 3 | # whitespace is consumed 4 | $(info b=$(basename src/foo.c src-1.0/bar qux.c hacks)) 5 | 6 | x=aa.a bb.b cc.c dd.d ee.e ff.f gg.g 7 | $(info x=$(basename $x)) 8 | 9 | @:;@: 10 | 11 | -------------------------------------------------------------------------------- /tests/addsuffix.mk: -------------------------------------------------------------------------------- 1 | $(info $(addsuffix .c,foo bar)) 2 | $(info $(addsuffix .c,foo,bar)) 3 | 4 | ext=.c 5 | $(info $(addsuffix $(ext),foo bar)) 6 | 7 | dot=. 8 | c=c 9 | $(info $(addsuffix $(dot)$(c),foo bar)) 10 | 11 | $(info $(addsuffix c c,foo bar)) 12 | 13 | @:;@: 14 | 15 | -------------------------------------------------------------------------------- /tests/ignoreandsilent.mk: -------------------------------------------------------------------------------- 1 | # GNU Make sez a recipe with leading '-' means ignore the error. 2 | # A recipe with a leading '@' doesn't echo the command 3 | # 4 | all: 5 | -exit 1 6 | -@exit 1 7 | @-exit 1 8 | @@echo foo 9 | @@-echo bar 10 | @echo baz 11 | ---exit 1 12 | @@echo foo 13 | 14 | -------------------------------------------------------------------------------- /tests/test_empty_variable_name.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | # 4 | 5 | # test expressions that GNU Make throws error "empty variable name" 6 | 7 | def test_one(): 8 | # raise NotImplementedError("TODO") 9 | pass 10 | -------------------------------------------------------------------------------- /tests/lastword.mk: -------------------------------------------------------------------------------- 1 | $(info $(lastword foo bar baz)) 2 | 3 | # commas mean nothing 4 | a=a,b,c,d,e,f,g,h,i,j 5 | $(info 1 out=$(lastword $(a))) 6 | 7 | a=a b c d e f g h i j 8 | $(info 2 out=$(lastword $(a))) 9 | 10 | a=1 11 | b=3 12 | c=8 13 | x=$(lastword $a,$b,$c) 14 | $(info last=$(x)) 15 | 16 | @:;@: 17 | -------------------------------------------------------------------------------- /tests/test_varname.py: -------------------------------------------------------------------------------- 1 | import run 2 | 3 | # check for errors in variable names 4 | # TODO finish this test 5 | 6 | def test1(): 7 | makefile=""" 8 | FOO BAR:=42 9 | @:;@: 10 | """ 11 | # error empty variable name 12 | # run.pymake_should_fail(makefile) 13 | run.gnumake_should_fail(makefile) 14 | 15 | -------------------------------------------------------------------------------- /tests/override.mk: -------------------------------------------------------------------------------- 1 | override variable = value 2 | 3 | override CFLAGS += -Wall 4 | 5 | # override RHS must be an assignment 6 | #override CC 7 | 8 | # bare override not allowed 9 | #override 10 | 11 | # TODO override and define 12 | #override define foo = 13 | #bar 14 | #endef 15 | 16 | include smallest.mk 17 | 18 | -------------------------------------------------------------------------------- /tests/recursive.mk: -------------------------------------------------------------------------------- 1 | # tinkering with recursive variable expansion 2 | # 3 | # deep recursively expanded vars in one expression 4 | A=a 5 | B=b 6 | C=c 7 | D=d 8 | E=e 9 | F=f 10 | AB=$A$B 11 | ABC=$(AB)$C 12 | ABCD=$(ABC)$D 13 | ABCDE=$(ABCD)$E 14 | ABCDEF=$(ABCDE)$F 15 | $(info ABCDEF=$(ABCDEF)) 16 | 17 | @:;@: 18 | 19 | -------------------------------------------------------------------------------- /pymake/debug.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | 3 | # start of useful debugger tools 4 | # 5 | __all__ = [ 6 | "get_line_number", 7 | ] 8 | 9 | def get_line_number(o): 10 | # get_pos() returns tuple 11 | # [0] is filename, [1] is tuple (row, column) 12 | # Want the line number. 13 | return o.get_pos()[1][0] 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/inpath.mk: -------------------------------------------------------------------------------- 1 | # search PATH for a file 2 | # (having fun tinkering with many functions) 3 | 4 | define inpath 5 | $(if $(filter $(1),$(notdir $(foreach p,$(subst :, ,$(PATH)),$(wildcard $p/*)))),,$(error $(1) not in path)) 6 | endef 7 | 8 | ifdef TEST_ME 9 | $(call inpath,gcc) 10 | $(call inpath,arm-marvell-linux-gcc) 11 | 12 | endif 13 | 14 | -------------------------------------------------------------------------------- /tests/confusing-directive.mk: -------------------------------------------------------------------------------- 1 | # legal 2 | #ifdef: ; @echo $@ 3 | 4 | # not legal 5 | #ifdef : ; @echo $@ 6 | 7 | # legal 8 | # ifdef:;@echo $@ 9 | 10 | # legal 11 | #ifdef = 42 12 | #$(info $(ifdef)) 13 | 14 | ifdef foo 15 | $(error foo) 16 | endif 17 | 18 | # legal 19 | ifdef=54 20 | $(info $(ifdef)) 21 | 22 | # legal 23 | #ifdef:=42 24 | 25 | @:;@: 26 | 27 | -------------------------------------------------------------------------------- /tests/flavor.mk: -------------------------------------------------------------------------------- 1 | # env vars show up as recursive? weird. 2 | $(info PATH=$(flavor PATH)) 3 | $(info TERM=$(flavor TERM)) 4 | 5 | a=a 6 | $(info a=$(flavor a)) 7 | 8 | b:=b 9 | $(info b=$(flavor b)) 10 | 11 | # undefined (treated as 'a b') 12 | $(info a b=$(flavor a b)) 13 | 14 | # whitespace is collapsed 15 | $(info a b =$(flavor a b )) 16 | 17 | @:;@: 18 | 19 | -------------------------------------------------------------------------------- /tests/word.mk: -------------------------------------------------------------------------------- 1 | #$(info $(word 0, foo bar baz, 0)) # *** first argument to 'word' function must be greater than 0. Stop. 2 | $(info $(word 1, foo bar baz)) 3 | $(info $(word 2, foo bar baz)) 4 | $(info $(word 3, foo bar baz)) 5 | $(info $(word 4, foo bar baz)) # empty 6 | 7 | $(info $(word 1, foo bar baz)) 8 | $(info $(word 1, foo bar baz)) 9 | 10 | @:;@: 11 | 12 | -------------------------------------------------------------------------------- /tests/envvar.mk: -------------------------------------------------------------------------------- 1 | # test for environment variables 2 | 3 | # Good to know. From GNU Make src/main.c: 4 | # /* By default, export all variables culled from the environment. */ 5 | # 6 | 7 | path = $(PATH) 8 | $(info path=$(PATH)) 9 | $(info path=$(subst :, ,${PATH})) 10 | 11 | $(foreach dir,\ 12 | $(subst :, ,$(PATH)),\ 13 | $(info dir=$(dir))\ 14 | ) 15 | 16 | @:;@: 17 | 18 | -------------------------------------------------------------------------------- /tests/rule_without_target.mk: -------------------------------------------------------------------------------- 1 | FOO:=1 2 | 3 | # because we haven't seen a Recipe yet, this is treated as just a regular line. 4 | ifdef FOO 5 | $(info FOO=$(FOO)) 6 | endif 7 | 8 | # "rule without a target" for 9 | # "compatibility with SunOS 4 make" 10 | : foo 11 | echo error\! should not see this 12 | exit 1 13 | 14 | foo: 15 | ifdef FOO 16 | @echo foo 17 | endif 18 | 19 | -------------------------------------------------------------------------------- /tests/makefile_list.mk: -------------------------------------------------------------------------------- 1 | # from the GNU Make manual 2 | # (slightly modified to use /dev/null for second filename) 3 | 4 | name1 := $(lastword $(MAKEFILE_LIST)) 5 | include /dev/null 6 | name2 := $(lastword $(MAKEFILE_LIST)) 7 | 8 | $(info origin=$(origin MAKEFILE_LIST)) 9 | $(info MAKEFILE_LIST=$(MAKEFILE_LIST)) 10 | 11 | all: 12 | @echo name1 = $(name1) 13 | @echo name2 = $(name2) 14 | 15 | -------------------------------------------------------------------------------- /tests/counted.mk: -------------------------------------------------------------------------------- 1 | # Counted loop (e.g. for loop) in Make 2 | # 3 | # davep 05-Nov-2014 4 | 5 | include range.mk 6 | 7 | ifdef TEST_ME 8 | $(info loop=*$(call range,10)*) 9 | $(info loop=*$(call range,100)*) 10 | #$(info loop=*$(call range,10000,0)*) 11 | 12 | #$(foreach i,$(call range,10,0),$(info loop 10 times i=$i)) 13 | $(foreach i,$(call range,10),$(info loop 10 times i=$i)) 14 | 15 | @:;@: 16 | endif 17 | 18 | -------------------------------------------------------------------------------- /pymake/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | __all__ = [ 4 | "Version", 5 | ] 6 | 7 | # for now, focus on 4.1 compatibility 8 | class Version(object): 9 | major = 4 10 | minor = 1 11 | 12 | # major = 3 13 | # minor = 81 14 | 15 | @classmethod 16 | def vstring(cls): 17 | return "%d.%d" % (cls.major, cls.minor) 18 | 19 | # TODO add methods, etc, to easily compare version numbers 20 | -------------------------------------------------------------------------------- /tests/mkseq.mk: -------------------------------------------------------------------------------- 1 | # make a sequence of characters of a certain length 2 | # $1 - number of elements in sequence 3 | # $2 - character(s) to sequence 4 | # 5 | # works by using $(word) to find the $1'th element in the list 6 | # if $1'th element not found, recursively call with a list 7 | # 8 | # Handy for counted loops. 9 | define mkseq 10 | $(if $(word $(1),$(2)),$(2),$(call mkseq,$(1),$(firstword $(2)) $(2))) 11 | endef 12 | 13 | -------------------------------------------------------------------------------- /tests/addprefix.mk: -------------------------------------------------------------------------------- 1 | $(info $(addprefix src/,foo bar)) 2 | 3 | $(info $(addprefix src/,foo,bar)) 4 | 5 | # TODO *** insufficient number of arguments (1) to function 'addprefix'. Stop. 6 | #$(info $(addprefix src/)) 7 | 8 | $(info $(addsuffix .c,$(addprefix src/,foo bar))) 9 | 10 | $(info $(addprefix $(HOME)/build/,$(addsuffix .c,$(addprefix src/,foo bar)))) 11 | 12 | $(info $(addprefix c c,foo bar)) 13 | 14 | @:;@: 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/dir.mk: -------------------------------------------------------------------------------- 1 | $(info $(dir src/foo.c hacks)) 2 | 3 | x=$(dir /etc/passwd) 4 | $(info x=$(x)) 5 | 6 | x=$(dir /etc/) 7 | $(info /etc/=$(x)) 8 | 9 | x=$(dir /tmp) 10 | $(info tmp=$(x)) 11 | 12 | x=$(dir tmp) 13 | $(info tmp=$(x)) 14 | 15 | me=$(PWD)/dir.mk 16 | 17 | $(info me=$(me) -> $(dir $(me))) 18 | 19 | me=$(PWD)///////me 20 | $(info me=$(dir $(me))) 21 | 22 | $(info slashy=$(dir //////tmp//////foo.txt)) 23 | 24 | @:;@: 25 | 26 | -------------------------------------------------------------------------------- /tests/if.mk: -------------------------------------------------------------------------------- 1 | out=(if a,b,c,d,e,f,g,h,i,j) 2 | $(info out=$(out)) 3 | 4 | out=$(if $(a)a,b,c,d,e,f,g,h,i,j) 5 | $(info out=$(out)) 6 | 7 | a=1 8 | b=2 9 | c=3 10 | d=4 11 | e=5 12 | f=6 13 | g=7 14 | h=8 15 | i=9 16 | j=10 17 | 18 | out=$(if $a,$b,c,d,e,f,g,h,i,j) 19 | $(info out=$(out)) 20 | 21 | a:=# 22 | out=$(if $a,$b,$c,$d,$e,$f,$g,$h,$i,$j) 23 | $(info out=$(out)) 24 | 25 | $(if 1,$(info 1is1),$(error 1 != 1)) 26 | 27 | @:;@: 28 | 29 | -------------------------------------------------------------------------------- /tests/sort.mk: -------------------------------------------------------------------------------- 1 | $(info $(sort z y x b c a)) 2 | $(info $(sort z y x b c a)) 3 | $(info $(sort z y x b c a)) 4 | 5 | x=x 6 | y=y 7 | z=z 8 | $(info $(sort a $x b $y c $z)) 9 | 10 | $(info $(sort a $x $x b $y $y c $z $z)) 11 | $(info $(sort a $x$x b $y$y c $z$z)) 12 | 13 | $(info $(sort $(shell cat /etc/passwd))) 14 | 15 | $(info $(sort $(wildcard *.py))) 16 | 17 | $(info $(sort $(sort a $x $x b $y $y c $z $z))) 18 | 19 | @:;@: 20 | 21 | -------------------------------------------------------------------------------- /pymake/flatten.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | # https://docs.python.org/3/library/itertools.html#itertools.chain.from_iterable 4 | def flatten(list_of_lists): 5 | "Flatten one level of nesting" 6 | return itertools.chain.from_iterable(list_of_lists) 7 | 8 | if __name__ == '__main__': 9 | a = ((6,7),(1,),(2,),(3,),(4,),(5,)) 10 | print(list(flatten(a))) 11 | 12 | a = [["functions*.py"],["*.mk"]] 13 | print(list(flatten(a))) 14 | 15 | -------------------------------------------------------------------------------- /tests/notdir.mk: -------------------------------------------------------------------------------- 1 | $(info $(notdir src/foo.c hacks)) 2 | 3 | x=$(notdir /etc/passwd) 4 | $(info x=$(x)) 5 | 6 | x=$(notdir /etc/) 7 | $(info /etc/=$(x)) 8 | 9 | x=$(notdir /tmp) 10 | $(info tmp=$(x)) 11 | 12 | x=$(notdir tmp) 13 | $(info tmp=$(x)) 14 | 15 | me=$(PWD)/dir.mk 16 | 17 | $(info me=$(me) -> $(notdir $(me))) 18 | 19 | me=$(PWD)///////me 20 | $(info me=$(notdir $(me))) 21 | 22 | $(info slashy=$(notdir //////tmp//////foo.txt)) 23 | 24 | 25 | @:;@: 26 | 27 | -------------------------------------------------------------------------------- /tests/functions_fs.mk: -------------------------------------------------------------------------------- 1 | # need a test that will always work correctly no matter what system runs the 2 | # test and what code might change over time. 3 | # 4 | # use $(sort) on $(wildcard) to guarantee file order 5 | 6 | pyfiles := $(sort $(wildcard functions*.py)) 7 | $(info pyfiles=$(pyfiles)) 8 | 9 | pyfiles := $(sort $(wildcard ../pymake/*.py)) 10 | $(info pyfiles=$(pyfiles)) 11 | 12 | pattern=*.py 13 | files := $(sort $(wildcard $(pattern))) 14 | $(info files=$(files)) 15 | 16 | @:;@: 17 | -------------------------------------------------------------------------------- /tests/file.mk: -------------------------------------------------------------------------------- 1 | # $(file) VarRef vs $(file ) Function 2 | file=hello.c 3 | include= $(shell cat $(file) | grep include) 4 | $(info include=$(include)) 5 | 6 | test=$(file < firstword.mk) 7 | $(info $(words $(test))) 8 | 9 | dollar=$$ 10 | $(info dollar=$(dollar)) 11 | 12 | openparen=( 13 | 14 | # convert $(firstword) tests to $(lastword) tests 15 | $(file > out.mk, $(subst $(dollar)$(openparen)firstword,$(dollar)$(openparen)lastword,$(test))) 16 | $(info wrote out.mk from firstword.mk) 17 | 18 | @:;@: 19 | -------------------------------------------------------------------------------- /tests/automatic.mk: -------------------------------------------------------------------------------- 1 | # test automatic variables 2 | # TODO many more tests 3 | 4 | .PHONY: all foo bar baz quz 5 | all: foo ; @echo all running rule $@ 6 | 7 | foo: bar baz qux ; @echo running rule $@ 8 | @echo foo prereq list $^ 9 | @echo foo first prereq $< 10 | 11 | # $^ eliminates duplicates 12 | # $+ reports all prereqs in order 13 | bar: baz qux baz baz 14 | @echo bar prereq list no dups $^ 15 | @echo bar prereq list w/ dups $+ 16 | 17 | baz: ; @echo running rule $@ 18 | 19 | qux: ; @echo running rule $@ 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/built-ins.mk: -------------------------------------------------------------------------------- 1 | # peek at GNU Make's built-in variable names 2 | # 3 | # Also run with -R "Disable the built-in variable settings" 4 | 5 | $(info .VARIABLES=$(.VARIABLES)) 6 | 7 | # as of GNU Make 4.3 8 | BUILTIN:=.VARIABLES .LOADED .INCLUDE_DIRS .SHELLFLAGS .DEFAULT_GOAL\ 9 | .RECIPEPREFIX .FEATURES VPATH SHELL MAKESHELL MAKE MAKE_VERSION MAKE_HOST\ 10 | MAKEFLAGS GNUMAKEFLAGS MAKECMDGOALS CURDIR SUFFIXES .LIBPATTERNS\ 11 | 12 | $(foreach var,$(BUILTIN),$(info $(var)=$($(var)) from $(origin $(var)))) 13 | 14 | @:;@: 15 | -------------------------------------------------------------------------------- /tests/variables.mk: -------------------------------------------------------------------------------- 1 | # Test the $(.VARIABLES) and all other built-in varibles. 2 | # TODO only the start. Need to add built-in support to pymake first. 3 | 4 | $(info $(.VARIABLES)) 5 | $(info $(.FEATURES)) 6 | 7 | $(info $(LINK.c)) 8 | $(info $(LINK.f)) 9 | $(info $(LINK.p)) 10 | $(info $(LINK.r)) 11 | 12 | #include math.mk 13 | 14 | # holy cheese this is super useful 15 | $(info MAKEFILE_LIST=$(MAKEFILE_LIST)) 16 | 17 | $(info DEFAULT_GOAL=$(DEFAULT_GOAL)) 18 | 19 | $(info RECIPE_PREFIX=$(.RECIPE_PREFIX)) 20 | 21 | @:;@: 22 | 23 | -------------------------------------------------------------------------------- /tests/words.mk: -------------------------------------------------------------------------------- 1 | $(info $(words foo bar baz)) 2 | $(info $(words foo bar baz )) 3 | $(info $(words a b c d e f g h i j k l m n o p q r s t u v w x y z)) 4 | 5 | foo= 6 | $(info $(words $(foo))) 7 | 8 | $(info $(words )) 9 | 10 | # "Returns the number of words in text. Thus, the last word of text is:" 11 | # -- gnu make manual 12 | text := A B D E F G H I J K L M N O P Q R S T U V W X Y Z _ ! @ $$ % ^ & * ( ) iamlast 13 | $(info $(words $(text))) 14 | $(info $(word $(words $(text)),$(text))) 15 | 16 | @:;@: 17 | 18 | -------------------------------------------------------------------------------- /tests/space.mk: -------------------------------------------------------------------------------- 1 | # creating a space, from the GNU Make manual 2 | blank:= # 3 | space := ${blank} ${blank} 4 | 5 | $(info blank=>>$(blank)<<) 6 | $(info space=>>$(space)<<) 7 | 8 | # leading whitespace eaten, intermediate whitesapce preserved, 9 | # trailing whitespace preserved (save output to file to verify) 10 | $(info 5 10 ) 11 | 12 | $(info 01234567890123456789) 13 | # should see four leading spaces even though $(info) followed by many more 14 | # spaces 15 | $(info $(subst q,${space},qqqq)<<) 16 | 17 | @:;@: 18 | 19 | -------------------------------------------------------------------------------- /tests/realpath.mk: -------------------------------------------------------------------------------- 1 | x=$(realpath /etc/passwd) 2 | $(info x=$(x)) 3 | 4 | x=$(realpath /lib) 5 | $(info x=$(x)) 6 | 7 | x=$(realpath /lib /etc/passwd /tmp) 8 | $(info x=$(x)) 9 | 10 | # does not exist -> empty 11 | $(info dave=$(realpath /etc/dave)) 12 | 13 | $(info $(realpath ../)) 14 | $(info $(realpath ../../)) 15 | 16 | # this is not portable across tests (pid will be different) 17 | #$(info $(realpath /proc/self)) 18 | 19 | $(info $(realpath /lib /lib32 /lib64 /bin)) 20 | 21 | $(info $(realpath $(sort $(wildcard /lib/*.so)))) 22 | 23 | @:;@: 24 | 25 | -------------------------------------------------------------------------------- /tests/suffix.mk: -------------------------------------------------------------------------------- 1 | $(info c=$(suffix src/foo.c src-1.0/bar.c hacks .c)) 2 | 3 | $(info dot=$(suffix .ssh)) 4 | $(info dot=$(suffix ......foo.....c)) 5 | $(info dot=$(suffix ......foo.....c...c.c)) 6 | $(info dot=$(suffix ..)) 7 | 8 | $(info long=$(suffix .thisisalongnamethatviolatescommonsense)) 9 | 10 | # weird, this evaluates to empty ; why? 11 | $(info weird=$(suffix foo.c/bar)) 12 | 13 | $(info path=$(suffix /this/is/a/test/foo.c)) 14 | 15 | $(info mydir=$(suffix $(sort $(wildcard *.py *.mk)))) 16 | 17 | $(info empty=>>$(suffix foo bar baz qux)<<) 18 | 19 | @:;@: 20 | 21 | -------------------------------------------------------------------------------- /tests/submake.mk: -------------------------------------------------------------------------------- 1 | # simple tests of sub-make 2 | 3 | $(info CURDIR=$(CURDIR)) 4 | 5 | all: 6 | @echo hello from make pid=$$$$ 7 | $(MAKE) -f $(CURDIR)/tests/submake.mk submake A=B B=C C=D 8 | -$(MAKE) -f $(CURDIR)/tests/submake.mk submake-error 9 | 10 | # adding a shell expression to verify we go through the shell 11 | @$(MAKE) -f $(CURDIR)/tests/submake.mk hello-world NUM=$$((10+20)) a={1,2,3} 12 | 13 | submake: 14 | @echo hello from submake pid=$$$$ 15 | 16 | hello-world: 17 | @echo hello, world NUM=$(NUM) a=$(a) 18 | 19 | submake-error: 20 | @echo error && exit 1 21 | 22 | -------------------------------------------------------------------------------- /tests/verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | # utilities to compare test results 5 | 6 | import itertools 7 | 8 | def compare_result_stdout(expect, actual): 9 | # result is an iterable of strings expected in the output 10 | # actual is a single string of stdout/stderr with '\n' between the lines 11 | 12 | linelist = ( s.strip() for s in actual.split("\n") ) 13 | 14 | # use zip_longest() to assure equal length 15 | fail = [ (a,b) for a,b in itertools.zip_longest(expect,linelist) if a != b ] 16 | assert not fail, fail 17 | 18 | -------------------------------------------------------------------------------- /tests/vpath.mk: -------------------------------------------------------------------------------- 1 | 2 | # "Specify the search path directories for file names that match pattern." 3 | vpath %.h ../headers 4 | 5 | # "Clear out the search path associated with pattern." 6 | vpath %.h 7 | 8 | # "Clear all search paths previously specified with vpath directives" 9 | vpath 10 | 11 | # this is valid for some reason (not sure what happens inside Make) 12 | vpath a=b 13 | vpath a:=b 14 | 15 | # assignment of var named "vpath" 16 | vpath=42 17 | ifndef vpath 18 | $(error vpath should be a variable) 19 | endif 20 | ifneq ($(vpath),42) 21 | $(error vpath != 42) 22 | endif 23 | 24 | include smallest.mk 25 | 26 | -------------------------------------------------------------------------------- /tests/hello.mk: -------------------------------------------------------------------------------- 1 | # this is a comment 2 | 3 | CC=gcc 4 | CFLAGS=-g -Wall 5 | 6 | EXE=hello 7 | OBJ=hello.o 8 | 9 | FOO:=bar 10 | 11 | all: $(EXE) 12 | # should see FOO=BAZ (assigned at bottom of file) because all the file is 13 | # parsed/run before rules are executed 14 | echo FOO=${FOO} 15 | 16 | hello : hello.o 17 | $(CC) $(CFLAGS) -o $@ $^ 18 | 19 | hello.o : hello.c 20 | $(CC) $(CFLAGS) -c -o $@ $^ 21 | 22 | clean : ; $(RM) $(OBJ) $(EXE) 23 | 24 | $(info RM=$(RM)) 25 | 26 | # make parses/runs whole file before starting on the rules 27 | # (will see this message before rules are run) 28 | $(info end of file bye now come again) 29 | 30 | FOO:=BAZ 31 | 32 | -------------------------------------------------------------------------------- /tests/deref.mk: -------------------------------------------------------------------------------- 1 | # Is there any way to deref something into a function call? 2 | # So far, doesn't seem like it. Which will make my evaluate much much simpler 3 | # but makes make less powerful, IMO. 4 | info foo=99 5 | a=info 6 | #$(info$(call $a, foo)) 7 | # 8 | #$(call $a,foo) 9 | 10 | # intuitively these lines should be the same (hint: they're not) 11 | $(info **$(info foo)**) # foo\n**** 12 | $(info **$($(a) foo)**) # **99** 13 | 14 | # this is how a is deref'd to info 15 | $(call $(a),this is from $$call) 16 | 17 | # function call not the above "info foo" variable 18 | $(info foo) 19 | 20 | # if I ever get a tattoo, i'll have this on my butt. 21 | @:;@: 22 | 23 | -------------------------------------------------------------------------------- /tests/tabs.mk: -------------------------------------------------------------------------------- 1 | # How does GNU Make like inside rule/assignments? 2 | # davep 24-Sep-2014 3 | 4 | all : ; @echo = all=$@ 5 | 6 | # assignment - just another whitespace? 7 | abc def = embedded tab 8 | $(info = embedded tab=$(abc def)) 9 | 10 | # not happy in rules; treated as whitepace => separate targets 11 | # The tab\ttab target is warning "target 'tab' given more than once in the same rule" 12 | # Weird! Make only reports this warning if a target explicitly given on cmdline 13 | tabtabtab : tab tab 14 | tab tab : ; @echo $@ 15 | 16 | # whitespace in prereq? or literal ? 17 | # treated as whitespace 18 | tabtab2 : tab2 tab3 19 | tab2 tab3: ; @echo I am $@ 20 | 21 | -------------------------------------------------------------------------------- /tests/strip.mk: -------------------------------------------------------------------------------- 1 | x=a b c d e f g 2 | $(info $(strip $x)) 3 | 4 | x=a b c d e f g 5 | $(info $(strip $x)) 6 | 7 | x= a b c d e f g # spaces at end 8 | $(info $(strip $x)) 9 | $(info $(strip $x $x $x)) 10 | $(info $(sort $(strip $x $x $x))) 11 | $(info $(filter a e,$(sort $(strip $x $x $x)))) 12 | 13 | x= aa bb cc dd ee ff gg # spaces at end 14 | $(info $(strip $x)) 15 | $(info $(strip $x $x $x)) 16 | $(info $(sort $(strip $x $x $x))) 17 | $(info $(filter aa ee,$(sort $(strip $x $x $x)))) 18 | 19 | x= a b c d e f g 20 | $(info $(strip $x)) 21 | 22 | # missing args 23 | x:= 24 | $(info >>$(strip )<<) 25 | $(info >>$(strip )<<) 26 | 27 | @:;@: 28 | 29 | -------------------------------------------------------------------------------- /tests/test_shellflags.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | import run 5 | 6 | def test_simple(): 7 | makefile=""" 8 | $(info .SHELLFLAGS=$(.SHELLFLAGS)) 9 | @:;@: 10 | """ 11 | a = run.gnumake_string(makefile) 12 | 13 | b = run.pymake_string(makefile) 14 | 15 | assert a==b 16 | 17 | # the -x flag will echo the command to stderr before executing it 18 | # (very useful when debugging) 19 | def test_x_flag(): 20 | makefile=""" 21 | .SHELLFLAGS+=-x 22 | $(info .SHELLFLAGS=$(.SHELLFLAGS)) 23 | top: 24 | echo .SHELLFLAGS=$(.SHELLFLAGS) 25 | """ 26 | a = run.gnumake_string(makefile) 27 | 28 | b = run.pymake_string(makefile) 29 | 30 | assert a==b 31 | 32 | -------------------------------------------------------------------------------- /tests/value.mk: -------------------------------------------------------------------------------- 1 | # from the gnu make manual; $PATH is correct vs $(PATH) 2 | FOO = $PATH 3 | P:=Error! 4 | 5 | #$(info info FOO=>>$(FOO)<<) 6 | $(info info value FOO=>>$(value FOO)<<) 7 | 8 | FOO,FOO=both the foos 9 | $(info info value FOO,FOO=>>$(value FOO,FOO)<<) 10 | A=1 11 | FOO,FOO1=both the foos 12 | $(info info value FOO,FOO-A=>>$(value FOO,FOO$A)<<) 13 | $(info info value FOO,FOO=>>$(value FOO,FOO)<<) 14 | 15 | BAR:=FOO 16 | $(info info value BAR=>>$(value BAR)<<) 17 | $(info info value $$BAR=>>$(value $(BAR))<<) 18 | 19 | formula=$(foreach foo,$(sort $(wildcard *.py),$(shell wc -l $(foo)))) 20 | $(info formula $(value formula)) 21 | 22 | @:;@: 23 | #all: 24 | # echo FOO=$(FOO) 25 | # echo value FOO=$(value (FOO)) 26 | -------------------------------------------------------------------------------- /tests/rule-specific-var.mk: -------------------------------------------------------------------------------- 1 | # Can I have rule specific assignments and recipes together? 2 | # NOPE. 3 | # 4 | # The "foo : FOO=BAR ; @echo bar bar bar" creates a variable 5 | # $(FOO)=="bar ; @echo bar bar bar" 6 | # 7 | # Verified 3.81 3.82 4.0 8 | 9 | abc : xyz ; def 10 | 11 | #vvvvvvvvvvvvvvvvvvvvvvv-------- part of $(FOO) 12 | foo : FOO=BAR ; echo oof oof oof 13 | #foo : ; @echo foo foo foo $(FOO) 14 | foo : \ 15 | ; @echo \ 16 | foo \ 17 | foo \ 18 | foo \ 19 | $(FOO) 20 | 21 | # cmd launched: echo foo foo foo ; echo oof oof off 22 | 23 | bar : BAR=FOO # hello I am a comment 24 | bar : # hello I am another comment 25 | @echo bar bar bar $(BAR) 26 | 27 | baz : BAZ\ 28 | =\ 29 | BAR 30 | baz : 31 | @echo baz baz baz $(BAZ) 32 | 33 | -------------------------------------------------------------------------------- /tests/join.mk: -------------------------------------------------------------------------------- 1 | $(info $(join a b,.c .o)) 2 | 3 | $(info $(join a b c d e f g ,.c .o)) 4 | 5 | $(info $(join a b ,.c .o .f .pas .cc .rs .ada)) 6 | $(info $(join a b ,.c .o .f .pas .cc .rs .ada)) 7 | $(info $(join a b ,.c .o .f .pas .cc .rs .ada )) 8 | 9 | a=a 10 | b=b 11 | c=c 12 | $(info $(join $a $b $c,$a $b $c)) 13 | a=aa aa 14 | b=bb bb 15 | c=cc cc 16 | $(info $(join $a $b $c,$a $b $c)) 17 | 18 | # "This function can merge the results of the dir and notdir functions, to produce 19 | # the original list of files which was given to those two functions." 20 | # -- GNU Make manual 4.3 Jan 2020 21 | 22 | filenames=/etc/passwd /etc/shadow /etc/group 23 | $(info $(join $(dir $(filenames)), $(notdir $(filenames)))) 24 | 25 | @:;@: 26 | 27 | -------------------------------------------------------------------------------- /tests/wildcard.mk: -------------------------------------------------------------------------------- 1 | # need a test that will always work correctly no matter what system runs the 2 | # test and what code might change over time. 3 | # 4 | # use $(sort) on $(wildcard) to guarantee file order 5 | 6 | #pyfiles := $(wildcard functions*.py) 7 | #$(info pyfiles=$(pyfiles)) 8 | 9 | files := $(sort $(wildcard *.py *.mk /usr/share/doc/make/*)) 10 | $(info files=$(files)) 11 | 12 | pyfiles := $(sort $(wildcard functions*.py)) 13 | $(info pyfiles=$(pyfiles)) 14 | 15 | pyfiles := $(sort $(wildcard functions*.py)) 16 | $(info pyfiles=$(pyfiles)) 17 | 18 | testfiles := $(sort $(wildcard test_* *.mk)) 19 | $(info testfiles=$(testfiles)) 20 | 21 | pattern=*.py 22 | patternfiles := $(sort $(wildcard $(pattern))) 23 | $(info patternfiles=$(patternfiles)) 24 | 25 | @:;@: 26 | 27 | -------------------------------------------------------------------------------- /tests/env_recursion.mk: -------------------------------------------------------------------------------- 1 | # Need a simple command that will give me the same response every time. 2 | # From the date man page: 3 | DATE:=date --date='TZ="America/Los_Angeles" 09:00 next Fri' 4 | 5 | # Example of loops in recursive variable references: ECHO requires value of NOW 6 | # but the $(shell) for NOW also requires ECHO. The 'printenv' exit value is 7 | # non-zero when the var doesn't exist. But under GNU Make 'ECHO' env var does 8 | # exist so we see 'ok' in the output. The value of NOW in ECHO will be an empty 9 | # string. 10 | # 11 | NOW = $(shell $(DATE) ; printenv ECHO && echo ok) 12 | ECHO = $(shell echo NOW is __$(NOW)__) 13 | 14 | 15 | export NOW ECHO 16 | 17 | $(info NOW=$(NOW)) 18 | $(info ECHO=$(ECHO)) 19 | 20 | @:;@: 21 | printenv ECHO 22 | printenv NOW 23 | 24 | -------------------------------------------------------------------------------- /tests/target-specific.mk: -------------------------------------------------------------------------------- 1 | # test target specific assignment 2 | CC:=gcc 3 | CFLAGS:=-Wall 4 | export CC FOO CFLAGS 5 | 6 | #$(info $(shell echo $$CC)) 7 | #$(info $(shell printenv CC)) 8 | 9 | BAZ:=baz 10 | 11 | all:CC:=clang 12 | all:FOO!=echo foo 13 | all:CFLAGS+=-g 14 | all:export PREREQ:=a.txt 15 | 16 | # PREREQ isn't eval'd so 'all' has no prereqs (the var only applies to the 17 | # recipe apparently) 18 | all: $(PREREQ) 19 | @echo BAR=$(BAR) 20 | @printenv CC 21 | @printenv FOO 22 | @printenv CFLAGS 23 | @printenv PREREQ 24 | all:BAR=$(BAZ) 25 | 26 | BAZ:=zab 27 | 28 | other:CFLAGS:=-O3 29 | other: 30 | @echo CC=$${CC} 31 | @echo FOO=$${FOO} 32 | @echo CFLAGS=$${CFLAGS} 33 | 34 | other: 35 | @echo CC=$${CC} 36 | @echo FOO=$${FOO} 37 | @echo CFLAGS=$${CFLAGS} 38 | 39 | -------------------------------------------------------------------------------- /tests/shellstatus.mk: -------------------------------------------------------------------------------- 1 | # should be 'undefined' 2 | $(info $(origin .SHELLSTATUS)) 3 | 4 | FOO := $(shell echo foo) 5 | 6 | # will now be 'override' (not sure why GNU Make chose that value) 7 | $(info $(origin .SHELLSTATUS)) 8 | 9 | ifneq ($(.SHELLSTATUS),0) 10 | $(error .SHELLSTATUS == $(.SHELLSTATUS)) 11 | endif 12 | 13 | BAR != echo bar 14 | $(info $(origin .SHELLSTATUS)) 15 | ifneq ($(.SHELLSTATUS),0) 16 | $(error .SHELLSTATUS == $(.SHELLSTATUS)) 17 | endif 18 | 19 | ERROR != exit 1 20 | $(info $(origin .SHELLSTATUS)) 21 | ifneq ($(.SHELLSTATUS),1) 22 | $(error .SHELLSTATUS == $(.SHELLSTATUS)) 23 | endif 24 | 25 | # recursive variable 26 | BAZ = $(shell echo baz) 27 | export BAZ 28 | 29 | $(info BAZ=$(BAZ) $(origin .SHELLSTATUS)) 30 | ifneq ($(.SHELLSTATUS),0) 31 | $(error .SHELLSTATUS == $(.SHELLSTATUS)) 32 | endif 33 | 34 | 35 | @:;@: 36 | 37 | -------------------------------------------------------------------------------- /tests/functions.mk: -------------------------------------------------------------------------------- 1 | # Only function calls. What happens? 2 | # 3 | # davep 07-Oct-2014 ; 4 | 5 | $(info hello, world) 6 | 7 | # legal (see "missing separator" notes below) 8 | $(if "$(FOO)",$(BAR),$(BAZ)) 9 | 10 | myinfo=info 11 | $(eval $(myinfo) hello, world) 12 | 13 | $(info $(.FEATURES)) 14 | 15 | $(warning warning warning danger danger!) 16 | 17 | foo=$(sort foo bar lose) 18 | 19 | #$(foreach prog 20 | 21 | feet=$(subst ee,EE,feet in the street) 22 | 23 | # NOT LEGAL -- "*** missing separator. Stop." 24 | # Seems to require a LHS. 25 | #$(sort foo bar lose) 26 | #$(or a,b,c) 27 | #$(and a,b,c) 28 | # Many more; need to have tests for all of them. 29 | 30 | a=$(or a,b,c) 31 | b=$(and a,b,c) 32 | 33 | c=$(dir ./tests) 34 | 35 | d=$(shell ls) 36 | 37 | e=$(join a b,.c .o) 38 | 39 | # need to have one target to prevent make from complaining 40 | @:;@: 41 | -------------------------------------------------------------------------------- /tests/call.mk: -------------------------------------------------------------------------------- 1 | reverse = $(2) $(1) 2 | foo = $(call reverse,a,b) 3 | 4 | $(info foo=$(foo)) 5 | $(info $(call reverse,1,2,3,4,5)) 6 | 7 | # variable override 8 | 1:=q 9 | $(info 1=$(1)) 10 | $(info $(call reverse,a,b)) 11 | $(info 1=$(1)) 12 | $(info $(call reverse,a)) 13 | 14 | # args start deep 15 | #deeparg = $(suffix $(5)) 16 | deeparg = $(suffix $5) 17 | $(info $(call deeparg,a.a,b.b,c.c,d.d,e.e,f.f)) 18 | # now using var instead of arg 19 | 5:=q.q 20 | $(info $(call deeparg,a.a)) 21 | 22 | # call something that does not exist 23 | $(info dave=$(call dave,a,b,c,d)) 24 | 25 | # are the args evaluated even if the cmd doesn't exist? 26 | # yes. these files do exist. 27 | a=$(shell echo a > /tmp/a.txt) 28 | b=$(shell echo b > /tmp/b.txt) 29 | c=$(shell echo c > /tmp/c.txt) 30 | d=$(shell echo d > /tmp/d.txt) 31 | $(info dave=$(call dave,$a,$b,$c,$d)) 32 | 33 | @:;@: 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/assign.mk: -------------------------------------------------------------------------------- 1 | # immediate 2 | a:=10 3 | $(info a=$a) 4 | 5 | # recursively expanded variable 6 | b=20 7 | c=30 8 | $(info b=$b c=$c) 9 | 10 | d1=$a $b $c 11 | d2:=$a $b $c 12 | $(info d1=$(d1) d2=$(d2)) 13 | 14 | # change the value of b, c 15 | b=40 16 | c=50 17 | $(info d1=$(d1) d2=$(d2)) 18 | 19 | # from the GNU Make 4.2 manual Jan 2020 20 | foo = $(bar) 21 | bar = $(ugh) 22 | ugh = Huh? 23 | $(info $(foo) $(bar) $(ugh)) 24 | 25 | FOO ?= bar 26 | $(info FOO=$(FOO)) 27 | 28 | BAR:=bar 29 | BAR ?= baz 30 | $(info BAR=$(BAR)) 31 | 32 | # error "Recursive variable 'CFLAGS' references itself (eventually)" 33 | #CFLAGS=-Wall -g 34 | #CFLAGS=$(CFLAGS) -O 35 | #$(info CFLAGS=$(CFLAGS)) 36 | 37 | # shell assign 38 | uname!=uname -a 39 | $(info uname=$(uname)) 40 | 41 | # POSIX make syntax of simply expanded assign 42 | q::=q 43 | r::=r 44 | s::=s 45 | $(info $q $r $s) 46 | 47 | 48 | @:;@: 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | *.swp 57 | 58 | log/fail.mk 59 | pymake.egg-info/ 60 | .pytest_cache/ 61 | 62 | .coverage* 63 | .tox/ 64 | -------------------------------------------------------------------------------- /tests/pattern.mk: -------------------------------------------------------------------------------- 1 | SRC=hello.c there.c all.c you.c rabbits.c 2 | OBJ=$(patsubst %.c,%.o,$(SRC)) 3 | 4 | $(info OBJ=$(OBJ)) 5 | 6 | $(info emptyc=$(patsubst %.c,%.o,.c .o)) 7 | 8 | $(info .S=$(patsubst %.c,%.o,$(patsubst %.c,%.S,$(SRC)))) 9 | $(info .h=$(patsubst %.S,%.h,$(patsubst %.c,%.o,$(patsubst %.c,%.S,$(SRC))))) 10 | 11 | $(info h=$(patsubst h%.c,%.o,$(SRC))) 12 | $(info hq=$(patsubst h%.c,q%.o,$(SRC))) 13 | $(info qh=$(patsubst h%.c,%q.o,$(SRC))) 14 | 15 | $(info hqq=$(patsubst %o.c,h%q.o,$(SRC))) 16 | 17 | $(info $(patsubst he%.c,h%.h,$(SRC))) 18 | 19 | # nothing should change (no wildcards) 20 | $(info $(patsubst c,h,$(SRC))) 21 | 22 | # whole string substitution 23 | $(info $(patsubst foo,bar,foo bar baz)) 24 | $(info $(patsubst foo,bar%,foo bar baz)) 25 | 26 | $(info $(patsubst %,bar,foo bar baz)) 27 | $(info $(patsubst f%,bar,foo bar baz)) 28 | 29 | $(info $(patsubst %,xyz%123,abcdef abcdqrst)) 30 | @:;@: 31 | -------------------------------------------------------------------------------- /examples/gnu_make.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | # 4 | # Run a block of lines through GNU Make to test for success/failure. 5 | 6 | 7 | import tempfile 8 | import subprocess 9 | 10 | def debug_save(lines): 11 | with open("/tmp/tmp.mk","w") as outfile: 12 | outfile.write("".join(lines)) 13 | 14 | def run_gnu_make(file_lines): 15 | # run my tests through GNU make to verify I'm testing a valid makefile 16 | with tempfile.NamedTemporaryFile(mode="w") as makefile: 17 | makefile.write("".join(file_lines)) 18 | # a single simple rule 19 | makefile.write("\n@:;@:\n") 20 | makefile.flush() 21 | capture_output = True # for debug, set False to release stdout/stderr 22 | capture_output = False 23 | subprocess.run(("make","-f",makefile.name), check=True, capture_output=capture_output) 24 | 25 | 26 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 202208017. Welcome to PyMake. 2 | 3 | Hello! Welcome! 4 | 5 | My goal is a source level debugger for GNU Makefiles. (Nothing against other 6 | Make implementations but I have to start somewhere.) The plan is to have a text 7 | interface like gdb. 8 | 9 | The project is very early. I can parse almost all of fully formed makefiles. I 10 | can output S-expressions, regenerate the makefile. 11 | 12 | As of this writing, I am implementing the GNU make $() functions. I cannot yet 13 | execute rules so I'm not yet actually a fully functioning Make. 14 | 15 | Example usage: 16 | # read a makefile, dumps incredible amounts of debugging while parsing. 17 | # Output of the makefile got to stdout 18 | python pymake.py Makefile 19 | 20 | # Example: parse/execute functions.mk, rewrite the makefile from the parsed 21 | # source to out.mk (very useful for seeing a cleaned makefile) 22 | python pymake.py -o out.mk functions.mk 23 | 24 | -------------------------------------------------------------------------------- /tests/lispy.mk: -------------------------------------------------------------------------------- 1 | # Honestly, car/cdr are close to everything all I know about Lisp. :-/ 2 | # 3 | # davep 30-Oct-2014 4 | 5 | define car 6 | $(firstword $(1)) 7 | endef 8 | 9 | define cdr 10 | $(wordlist 2,$(words $(1)),$(1)) 11 | endef 12 | 13 | ifdef TEST_ME 14 | list=a b c d e f g 15 | $(info list=$(list)) 16 | $(info car=$(call car,$(list))) 17 | $(info cdr=$(call cdr,$(list))) 18 | $(info cdr=$(call cdr,$(call cdr,$(list)))) 19 | $(info cdr=$(call cdr,$(call cdr,$(call cdr,$(list))))) 20 | $(info cdr=$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(list)))))) 21 | $(info cdr=$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(list))))))) 22 | $(info cdr=$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(list)))))))) 23 | $(info cdr=$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(call cdr,$(list))))))))) 24 | 25 | $(info car=$(call car,$(call cdr,$(list)))) 26 | 27 | lispy_all:;@: 28 | endif 29 | 30 | -------------------------------------------------------------------------------- /tests/test_eval.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024-2025 David Poole david.poole@ericsson.com 3 | # 4 | # test the $(eval) function 5 | 6 | import run 7 | 8 | def test1(): 9 | makefile=""" 10 | $(eval FOO:=foo) 11 | ifndef FOO 12 | $(error FOO is missing 13 | endif 14 | ifneq ($(FOO),foo) 15 | $(error FOO is wrong) 16 | endif 17 | @:;@: 18 | """ 19 | run.simple_test(makefile) 20 | 21 | def test_rule(): 22 | makefile=""" 23 | $(eval @:;@:) 24 | """ 25 | run.simple_test(makefile) 26 | 27 | def test_two_eval(): 28 | makefile=""" 29 | $(eval BAR:=bar) 30 | $(eval FOO:=$(BAR)) 31 | ifndef FOO 32 | $(error FOO is missing) 33 | endif 34 | ifneq ($(FOO),bar) 35 | $(error foo is wrong) 36 | endif 37 | @:;@: 38 | """ 39 | run.simple_test(makefile) 40 | 41 | def test_eval_return(): 42 | makefile=""" 43 | $(info >>$(eval FOO:=foo)<<) 44 | ifneq ($(FOO),foo) 45 | $(error foo is wrong) 46 | endif 47 | @:;@: 48 | """ 49 | run.simple_test(makefile) 50 | -------------------------------------------------------------------------------- /tests/firstword.mk: -------------------------------------------------------------------------------- 1 | $(info $(firstword foo bar baz)) 2 | 3 | # from the GNU make manual 4 | comma:= , 5 | empty:= 6 | space:= $(empty) $(empty) 7 | 8 | # commas mean nothing 9 | a=a,b,c,d,e,f,g,h,i,j 10 | $(info 1 out=$(firstword $(a))) 11 | 12 | a=a b c d e f g h i j 13 | $(info 2 out=$(firstword $(a))) 14 | 15 | a=a b c d e f g h i j 16 | $(info 3 out=$(firstword $(a))) 17 | 18 | a= a b c d e f g h i j 19 | $(info 3 out=$(firstword $(a))) 20 | 21 | b=e d c b a 22 | c=8 7 6 5 3 0 9 23 | $(info testme=$a,$b,$c) 24 | x=$(firstword $a,$b,$c) 25 | $(info first=$(x)) 26 | 27 | a=1 28 | b=3 29 | c=8 30 | $(info testme=$a,$b,$c) 31 | x=$(firstword $a,$b,$c) 32 | $(info first=$(x)) 33 | 34 | a=2 1 35 | $(info first=$(x)) 36 | 37 | x = $(firstword $a${space}$b${space}$c${space}) 38 | $(info spaces abc first=>>$(x)<<) 39 | 40 | x=$(firstword) 41 | $(info empty first=>>$(x)<<) 42 | 43 | x=$(firstword ${space} ${space} ${space}) 44 | $(info empty first=>>$(x)<<) 45 | 46 | @:;@: 47 | -------------------------------------------------------------------------------- /tests/test_builtins.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | # TODO just getting started with this file 5 | # I need to implement ifdef/ifndef first. 6 | 7 | import run 8 | 9 | _debug = True 10 | 11 | def test1(): 12 | makefile = """ 13 | ifndef MAKE_VERSION 14 | $(error missing MAKE_VERSION) 15 | endif 16 | 17 | $(info MAKE_VERSION=$(MAKE_VERSION)) 18 | @:;@: 19 | """ 20 | out1 = run.gnumake_string(makefile).strip() 21 | assert out1.startswith("MAKE_VERSION=") 22 | out1 = run.pymake_string(makefile).strip() 23 | assert out1.startswith("MAKE_VERSION=") 24 | 25 | def test_variables(): 26 | makefile = """ 27 | ifndef .VARIABLES 28 | $(error missing .VARIABLES) 29 | endif 30 | 31 | $(info $(.VARIABLES)) 32 | $(info $(origin .VARIABLES)) 33 | @:;@: 34 | """ 35 | out1 = run.gnumake_string(makefile) 36 | fields = out1.split("\n") 37 | assert fields[1] == "default" 38 | 39 | out1 = run.pymake_string(makefile) 40 | fields = out1.split("\n") 41 | assert fields[1] == "default" 42 | 43 | -------------------------------------------------------------------------------- /tests/filepatsubst.mk: -------------------------------------------------------------------------------- 1 | # testing $(VAR: ... ) 2 | # abbreviated $(patsubst) 3 | 4 | SRC=hello.c there.c all.c you.c rabbits.c 5 | 6 | # spaces are significant in the patterns 7 | # extr spaces in the source are eliminated 8 | $(info 1 $(SRC:.c=.o)) 9 | $(info 2 $(SRC:.c=.o )) # output filenames will have extra trailing space 10 | $(info 3 $(SRC: .c = .o qqq)) # arg1==" .c " and arg2==" .o qqq" 11 | 12 | # missing parts of the expression 13 | $(info m1 $(SRC:)) 14 | $(info m2 $(SRC:.c)) 15 | $(info m3 $(SRC:.c=)) 16 | $(info m4 $(SRC:.c=q)) 17 | $(info m5 $(SRC:c=q)) 18 | $(info m6 $(SRC:o.c=q)) 19 | 20 | # following works peachy, dammit 21 | ext=.c 22 | colon=: 23 | equal== 24 | $(info 4 $(SRC:$(ext)=.o)) 25 | $(info 5 $(SRC$(colon)$(ext)$(equal).o)) 26 | 27 | dot=. 28 | $(info 6 $(SRC$(colon)$(ext)$(equal)$(dot)o)) 29 | 30 | # add bunch of spaces to the extension 31 | FOO:=$(SRC:.c= .c) 32 | BAR:=$(FOO:.c= .c) 33 | $(info BAR=$(BAR)) 34 | # now remove the spaces 35 | BAZ:=$(BAR: .c=.o) 36 | $(info BAZ=$(BAZ)) 37 | BAA:=$(BAZ: .c=.o) 38 | $(info BAA=$(BAA)) 39 | 40 | @:;@: 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.platform }} 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | platform: [ubuntu-latest] 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 13 | env: 14 | PLATFORM: ${{ matrix.platform }} 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Set up Python {{ ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install tox tox-gh-actions 27 | 28 | - name: Test with tox 29 | run: tox -e py 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v1 33 | with: 34 | file: ./coverage.xml 35 | flags: unittests 36 | env_vars: PLATFORM,PYTHON 37 | name: codecov-umbrella 38 | fail_ci_if_error: false 39 | -------------------------------------------------------------------------------- /tests/origin.mk: -------------------------------------------------------------------------------- 1 | # Run this test makefile with: 2 | # make FOO=1 -e -f origin.mk 3 | # (I think) 4 | 5 | # undefined 6 | $(info undfined=$(origin undefined)) 7 | 8 | # environment 9 | $(info PATH=$(origin PATH)) 10 | 11 | # environment 12 | $(info TERM=$(origin TERM)) 13 | 14 | # override 15 | override TERM:=vt52 16 | $(info TERM=$(origin TERM)) 17 | 18 | # file 19 | a:=1 20 | $(info a=$(origin a)) 21 | 22 | # default 23 | $(info .VARIABLES=$(origin .VARIABLES)) 24 | $(info CC=$(origin CC)) 25 | 26 | $(info FOO=$(FOO) $(origin FOO)) 27 | FOO:=bar 28 | $(info FOO=$(FOO) $(origin FOO)) # value should not change 29 | override FOO:=bar 30 | $(info FOO=$(FOO) $(origin FOO)) # "bar override" 31 | 32 | # clear a variable 33 | $(info OLDPWD=$(OLDPWD) $(origin OLDPWD)) 34 | override OLDPWD= 35 | $(info OLDPWD=$(OLDPWD) $(origin OLDPWD)) 36 | 37 | # $@ does not exist outside a rule 38 | $(info @=$(origin @)) 39 | 40 | # TODO these need tests 41 | # errors " *** missing separator. Stop." 42 | #override 43 | #override qq 44 | #override =42 45 | #override 1+2 46 | 47 | all: 48 | @echo in rule origin @= $(origin @) 49 | 50 | -------------------------------------------------------------------------------- /tests/eval.mk: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024-2025 David Poole david.poole@ericsson.com 3 | # 4 | # test the eval function 5 | 6 | $(eval FOO:=foo) 7 | $(info a FOO=$(FOO)) 8 | $(eval $$(info b FOO=$$(FOO))) 9 | 10 | $(eval $$(info hello world 1)) 11 | 12 | HELLO:=$$(info hello world 2) 13 | $(eval $(HELLO)) 14 | 15 | FOO:=BAZ:=$$(shell echo baz) 16 | $(eval $(FOO)) 17 | $(info BAZ=$(BAZ)) 18 | 19 | FOO=$(1)=$$(shell echo $(1)) 20 | 21 | $(info $(call FOO,foo)) 22 | $(eval $(call FOO,foo)) 23 | $(eval $(call FOO,bar)) 24 | $(info foo=$(foo)) 25 | $(info bar=$(bar)) 26 | 27 | define large_comment 28 | $(info this is a contrived example) 29 | $(info showing a multi-line variable) 30 | endef 31 | 32 | $(eval $(large_comment)) 33 | 34 | FOO:=FOO error if you see this FOO 35 | BAR:=BAR error if you see this BAR 36 | 37 | define silly_example 38 | FOO:=foo 39 | BAR:=bar 40 | endef 41 | 42 | $(eval $(silly_example)) 43 | 44 | ifneq ($(FOO),foo) 45 | $(error FOO fail) 46 | endif 47 | 48 | ifneq ($(BAR),bar) 49 | $(error BAR fail) 50 | endif 51 | 52 | $(info FOO=$(FOO) BAR=$(BAR)) 53 | 54 | @:;@: 55 | 56 | -------------------------------------------------------------------------------- /tests/dot-posix.mk: -------------------------------------------------------------------------------- 1 | # .POSIX changes some of make's behaviors 2 | # 3 | # 28-sep-2014 4 | 5 | $(error TODO) 6 | 7 | .POSIX 8 | 9 | # 3.1.1 Splitting Long Lines 10 | # "Outside of recipe lines, backslash/newlines are converted into a single 11 | # space character. Once that is done, all whitespace around the 12 | # backslash/newline is condensed into a single space: this includes all 13 | # whitespace preceding the backslash, all whitespace at the beginning of the 14 | # line after the backslash/newline, and any consecutive backslash/newline 15 | # combinations." 16 | # 17 | # "If the .POSIX special target is defined then backslash/newline handling is 18 | # modified slightly to conform to POSIX.2: first, whitespace preceding a 19 | # backslash is not removed and second, consecutive backslash/newlines are not 20 | # condensed." 21 | # 22 | more-fun-in-assign\ 23 | = \ 24 | the \ 25 | leading \ 26 | and \ 27 | trailing\ 28 | white \ 29 | space \ 30 | should \ 31 | be \ 32 | eliminated\ 33 | \ 34 | \ 35 | \ 36 | including \ 37 | \ 38 | \ 39 | blank\ 40 | \ 41 | \ 42 | lines 43 | -------------------------------------------------------------------------------- /tests/test_define.mk: -------------------------------------------------------------------------------- 1 | # error empty variable name 2 | #define 3 | 4 | define simple-assign := 5 | @echo abc 6 | @echo 123 7 | endef 8 | 9 | define default-assign 10 | @echo def 11 | @echo 456 12 | endef 13 | 14 | define append-assign += 15 | @echo hij 16 | @echo 789 17 | endef 18 | 19 | define recursive-assign = 20 | @echo klm 21 | @echo 101112 22 | endef 23 | 24 | define shell-assign != 25 | uname -a ; 26 | which uname 27 | endef 28 | 29 | t=t 30 | l=l 31 | 32 | # warning: extraneous text after 'define' directive 33 | define $two$lines = q 34 | echo foo 35 | echo $(BAR) 36 | endef 37 | 38 | # ooooo interesting the blank line here is preserved 39 | # (without it, the echo $(BAR) echo $(BAZ) are on the same line 40 | # and are treated as echo "$(BAR) echo $(BAZ)" (2nd echo is a literal string 41 | # to the 1st echo) 42 | define twolines += 43 | 44 | echo $(BAZ) 45 | endef 46 | 47 | 48 | define tab-indent-assign 49 | echo oh tab where else can you sneak in 50 | endef 51 | 52 | define tab-define-tab-assign 53 | echo tab-define-tab 54 | endef 55 | 56 | $(info $(filter %-assign,$(.VARIABLES))) 57 | 58 | all: 59 | $(twolines) 60 | 61 | shell: 62 | @echo "$(shell-assign)" 63 | 64 | -------------------------------------------------------------------------------- /tests/test_assign.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | 4 | import run 5 | 6 | # Test all the ways we can assign a variable 7 | # 8 | # from the gnu make pdf: 9 | # immediate = deferred 10 | # immediate ?= deferred 11 | # immediate := immediate 12 | # immediate ::= immediate 13 | # immediate += deferred or immediate 14 | # immediate != immediate 15 | 16 | def run_test(makefile, expect): 17 | out = run.gnumake_string(makefile) 18 | print("out=",out) 19 | assert expect==out, out 20 | 21 | out = run.pymake_string(makefile) 22 | print("out=",out) 23 | 24 | def test1(): 25 | makefile = """ 26 | CC:=gcc 27 | $(info CC=$(CC)) 28 | @:;@: 29 | """ 30 | expect = "CC=gcc" 31 | run_test(makefile, expect) 32 | 33 | def test_add(): 34 | makefile = """ 35 | FOO:=foo 36 | FOO+=bar 37 | $(info FOO=$(FOO)) 38 | @:;@: 39 | """ 40 | expect = "FOO=foo bar" 41 | run_test(makefile, expect) 42 | 43 | def test_update(): 44 | makefile = """ 45 | FOO:=foo 46 | FOO:=$(FOO) foo 47 | FOO:=bar $(FOO) 48 | 49 | @:; @echo $(FOO) 50 | """ 51 | expect = "bar foo foo" 52 | run_test(makefile, expect) 53 | 54 | -------------------------------------------------------------------------------- /tests/undefine.mk: -------------------------------------------------------------------------------- 1 | FOO=1 2 | ifndef FOO 3 | $(error missing FOO) 4 | endif 5 | 6 | # will undefine a variable named 'FOO:=3' 7 | undefine FOO:=3 8 | ifndef FOO 9 | $(error missing FOO) 10 | endif 11 | $(info FOO=$(FOO)) 12 | 13 | undefine FOO 14 | ifdef FOO 15 | $(error FOO still lives) 16 | endif 17 | 18 | FOO:=1 19 | BAR:=2 20 | BAZ:=3 21 | ifndef FOO 22 | $(error missing FOO) 23 | endif 24 | ifndef BAR 25 | $(error missing BAR) 26 | endif 27 | ifndef BAZ 28 | $(error missing BAZ) 29 | endif 30 | 31 | # will undefine a variable named "FOO BAR BAZ" 32 | undefine FOO BAR BAZ 33 | ifndef FOO 34 | $(error FOO wrongly undefined) 35 | endif 36 | ifndef BAR 37 | $(error BAR wrongly undefined) 38 | endif 39 | ifndef BAZ 40 | $(error BAZ wrongly undefined) 41 | endif 42 | 43 | undefine FOO 44 | undefine BAR 45 | undefine BAZ 46 | ifdef FOO 47 | $(error FOO) 48 | endif 49 | ifdef BAR 50 | $(error BAR) 51 | endif 52 | ifdef BAZ 53 | $(error BAZ) 54 | endif 55 | 56 | # remove a built-in 57 | $(info .VARIABLES is from $(origin .VARIABLES)) 58 | undefine .VARIABLES 59 | $(info .VARIABLES=>>$(.VARIABLES)<< now from $(origin .VARIABLES)) 60 | ifdef .VARIABLES 61 | $(error .VARIABLES still lives) 62 | endif 63 | 64 | @:;@: 65 | 66 | -------------------------------------------------------------------------------- /tests/ifeq-nested.mk: -------------------------------------------------------------------------------- 1 | a=10 2 | b=10 3 | #c=10 4 | 5 | ifneq "$a" "$b" qqq 6 | $(info quote a = b) 7 | endif 8 | 9 | ifeq ($a,$b) 10 | $(info a=b) 11 | ifeq ($b,$c) 12 | $(info b=c) 13 | ifeq ($c,$d # missing close paren shouldn't be seen 14 | $(info c=d) 15 | else 16 | $(info I am invalid no closing paren 17 | endif 18 | else 19 | $(info b!=c) 20 | endif 21 | else 22 | $(info a!=b) 23 | endif 24 | 25 | # nest test 26 | ifeq '$(shell date)' '$(shell date)' 27 | $(info shell is fast) 28 | ifeq "$(firstword $(shell date))" "Wed" 29 | $(info It is Wednesday, my dudes.) 30 | else 31 | ifeq "$(firstword $(shell date))" "Fri" 32 | $(info TGIF!) 33 | else 34 | ifeq "$(firstword $(shell date))" "Mon" 35 | $(info I hate Mondays.) 36 | else 37 | ifeq "$(firstword $(shell date))" "Sat" 38 | $(info Time for yard work) 39 | else 40 | $(info boring day) 41 | endif 42 | endif 43 | endif 44 | endif 45 | else 46 | $(info shell is not fast) 47 | endif 48 | 49 | @:;@: 50 | 51 | -------------------------------------------------------------------------------- /tests/whitespace.mk: -------------------------------------------------------------------------------- 1 | # TODO pymake cannot yet parse this file 2 | 3 | # the next line is $ (gnu make ignores as of v4.3) 4 | $ 5 | 6 | # the next line is $ (gnu make ignores as of v4.3) 7 | $ 8 | 9 | # gnu make accepts and ignores these 10 | $() 11 | $( ) 12 | $(info $()) 13 | 14 | $( )foo=fooA 15 | $(info $$foo=$($( )foo) qq ) 16 | 17 | x:=a a a a a a a a a a a a a a a a a b 18 | 19 | a=a 20 | b=b 21 | 22 | xx=$x $x 23 | 24 | $(info $a$b $a $b $(xx) q q q $b $a $(shell seq 1 10 ) $a$(shell seq 2 2 12)$a , $(x)b $(x)a 1 2 3 4 ) 25 | $(info $(filter $a$b $a $b $(xx) q qq qqq $b $a $(shell seq 1 10 ) $a$(shell seq 2 2 12)$a , $(x)b $(x)a 1 2 3 4 )) 26 | 27 | # argv[0] == 28 | # "ab a b a a a a a a a a a a a a a a a a a b a a a a a a a a a a a a a a a a a b q q q b a 1 2 3 4 5 6 7 8 9 10 a2 4 6 8 10 12a " 29 | # argv[1] == 30 | # " a a a a a a a a a a a a a a a a a bb a a a a a a a a a a a a a a a a a ba 1 2 3 4 " 31 | # 32 | 33 | # dangling varref $ 34 | foo=bar $ 35 | $(info foo=$(foo)) 36 | 37 | # *** empty variable name. Stop. 38 | # = bar 39 | 40 | # whitespace is preserved 41 | # leading whitespace is discarded 42 | # there are three spaces after BAR 43 | FOO:= BAR 44 | $(info FOO=>>>$(FOO)<<<) 45 | # output is "FOO=>>>BAR <<<" 46 | 47 | @:;@: 48 | 49 | -------------------------------------------------------------------------------- /tests/test_warnings.py: -------------------------------------------------------------------------------- 1 | import run 2 | 3 | def run_test(makefile, expect): 4 | out = run.gnumake_string(makefile, flags=run.FLAG_OUTPUT_STDERR) 5 | print("out=",out) 6 | assert expect in out, out 7 | 8 | def test_warning_function(): 9 | makefile=""" 10 | $(warning this is your only warning) 11 | @:;@: 12 | """ 13 | run_test(makefile, "this is your only warning") 14 | 15 | def test_warning_ifeq_extraneous_text(): 16 | # extraneous text after 'ifeq' directive 17 | makefile=""" 18 | ifeq (a,a)q 19 | endif 20 | 21 | # same 22 | #ifeq (a,a), 23 | #ifeq (a,a)) 24 | 25 | @:;@: 26 | """ 27 | run_test(makefile, "extraneous text after 'ifeq' directive") 28 | 29 | def test_extraneous_text_after_else(): 30 | makefile=""" 31 | ifdef FOO 32 | else this should throw "Extraneous text after else directive" 33 | endif 34 | 35 | @:;@: 36 | """ 37 | run_test(makefile, "extraneous text after 'else' directive") 38 | 39 | def test_else_ifeq_whitespace(): 40 | # extraneous text after 'else' directive 41 | # (the ifeq isn't correctly parsed because of missing whitespace) 42 | makefile=""" 43 | # whitespace required. Sort of. "extraneous text after 'else' directive" 44 | ifeq ($(foo),1) 45 | else ifeq($(foo),2) 46 | endif 47 | @:;@: 48 | """ 49 | run_test(makefile, "extraneous text after 'else' directive") 50 | 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from pathlib import Path 4 | from setuptools import setup 5 | 6 | 7 | if __name__ == "__main__": 8 | setup( 9 | name="pymake", 10 | version="4.1.0", 11 | entry_points = { 12 | "console_scripts": ['pymake=pymake.pymake:main'] 13 | }, 14 | extras_require=dict(), 15 | description="Parse GNU Makefiles with Python", 16 | long_description=(Path(__file__).parent / "README.md").read_text(), 17 | author="linuxlizard", 18 | author_email="davep@mbuf.com", 19 | license="GNU General Public License v2.0", 20 | url="https://pymake.readthedocs.io", 21 | classifiers=[ 22 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Operating System :: OS Independent", 30 | ], 31 | scripts = [ 32 | # 'pymake/py-submake', 33 | ], 34 | packages=[ 35 | "pymake", 36 | ], 37 | setup_requires=["setuptools"], 38 | ) 39 | 40 | -------------------------------------------------------------------------------- /tests/append.mk: -------------------------------------------------------------------------------- 1 | # test the += operator 2 | 3 | # simply expanded 4 | FOO:=bar 5 | FOO+=baz 6 | $(info FOO=$(FOO)) 7 | 8 | # recursively expanded 9 | FOO=abc 10 | FOO+=xyz 11 | $(info FOO=$(FOO)) 12 | 13 | # recursively expanded 14 | BAR=$(FOO) 15 | BAR+=bar 16 | $(info BAR=$(BAR)) 17 | 18 | # now replace FOO; BAR should change 19 | FOO=pqr 20 | $(info BAR=$(BAR)) 21 | 22 | # append to doesn't exist 23 | BAZ+=baz 24 | $(info BAZ=$(BAZ)) 25 | 26 | PHRASE=feet on the street 27 | FOO=$(subst ee,EE,$(PHRASE)) 28 | 29 | FOO+=bar 30 | $(info FOO=$(FOO)) 31 | 32 | PHRASE=meet me in St Louis 33 | $(info FOO=$(FOO)) 34 | 35 | undefine FOO 36 | undefine BAR 37 | undefine PHRASE 38 | 39 | PHRASE=feet on the street 40 | FOO=$(subst ee,EE,$(PHRASE)) 41 | BAR=bar 42 | 43 | # multiple recursive variables (append recursive to recursive) 44 | FOO+=$(BAR) 45 | $(info FOO=$(FOO)) 46 | PHRASE=sleepwalking through engineering 47 | $(info FOO=$(FOO)) 48 | BAR=baz 49 | $(info FOO=$(FOO)) 50 | 51 | A=a 52 | B=b 53 | C=c 54 | D=d 55 | E=e 56 | F=f 57 | ABCDEF+=$A 58 | ABCDEF+=$B 59 | ABCDEF+=$C 60 | ABCDEF+=$D 61 | ABCDEF+=$E 62 | ABCDEF+=$F 63 | $(info ABCDEF=$(ABCDEF)) 64 | 65 | undefine FOO 66 | undefine BAR 67 | undefine BAZ 68 | undefine PHRASE 69 | 70 | # *** recursive variable 'FOO' references itself (eventually) 71 | FOO=foo 72 | BAR=bar 73 | FOO+=$(BAR) 74 | BAR+=$(FOO) 75 | $(info FOO=$(FOO)) 76 | 77 | @:;@: 78 | 79 | -------------------------------------------------------------------------------- /examples/statement.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) David Poole david.poole@ericsson.com 3 | 4 | # simple example showing how to use some of the components of pymake 5 | # run with: 6 | # PYTHONPATH=. python3 examples/statement.py 7 | # 8 | # davep 20241117 9 | 10 | import sys 11 | import logging 12 | 13 | logger = logging.getLogger("pymake") 14 | 15 | import pymake.source as source 16 | import pymake.vline as vline 17 | from pymake.scanner import ScannerIterator 18 | from pymake import tokenizer 19 | 20 | def main(infilename): 21 | src = source.SourceFile(infilename) 22 | src.load() 23 | 24 | # iterator across all actual lines of the makefile 25 | line_scanner = ScannerIterator(src.file_lines, src.name) 26 | 27 | # iterator across "virtual" lines which handles the line continuation 28 | # (backslash) 29 | vline_iter = vline.get_vline(src.name, line_scanner) 30 | 31 | for virt_line in vline_iter: 32 | s = str(virt_line) 33 | print(s,end="") 34 | vchar_scanner = iter(virt_line) 35 | stmt = tokenizer.tokenize_statement(vchar_scanner) 36 | print(stmt) 37 | 38 | if __name__ == '__main__': 39 | logging.basicConfig(level=logging.INFO) 40 | for infilename in sys.argv[1:]: 41 | main(infilename) 42 | else: 43 | print("usage: %s makefile1 [makefile2 [makefile3...]]", file=sys.stderr) 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/ifdef-recipe.mk: -------------------------------------------------------------------------------- 1 | # A recipe containing an ifdef block. 2 | # 3 | 4 | foo: 5 | @echo foo? 6 | ifdef FOO 7 | @echo hello from foo! 8 | endif # FOO 9 | @echo bar 10 | ifdef FOO 11 | $(info this FOO is not part of the recipe) 12 | endif 13 | 14 | bar: 15 | @echo bar? 16 | ifdef BAR 17 | @echo this is still part of the recipe; is an error but only if BAR defined 18 | endif 19 | @echo bar 20 | 21 | baz: 22 | @echo baz? 23 | ifdef BAZ 24 | $(info this BAZ is not part of the recipe) 25 | endif 26 | $(info this BAZ also not part of the recipe) 27 | 28 | $(info oh look a leading tab!) 29 | 30 | # $(info leading tab before any rules) # this fails 31 | 32 | # endef cannot have leading tab 33 | define DAVE 34 | 42 35 | endef 36 | 37 | # directives allowed with leading tab 38 | # export unexport vpath include -include sinclude load 39 | # and conditionals 40 | # (others?) 41 | export DAVE 42 | 43 | # variable assignment allowed w/ leading tab 44 | a := a 45 | 46 | : 47 | @echo no targets 48 | 49 | foo: 50 | @echo foo? 51 | ifdef FOO 52 | @echo hello from foo! 53 | endif # FOO 54 | @echo bar 55 | 56 | ifdef BAR 57 | $(info this is end of the rule when BAR is defined) 58 | endif 59 | 60 | $(info foo bar baz)echo foo bar baz 61 | echo foo 62 | 63 | @# captured as a recipe ("ifdef: No such file or directory") 64 | # ifdef BAZ 65 | # baz baz 66 | # endif 67 | 68 | b := b 69 | 70 | -------------------------------------------------------------------------------- /tests/info.mk: -------------------------------------------------------------------------------- 1 | # Fiddling with the info function. 2 | # 3 | # info, warning, error are allowed in contexts that other functions are not. 4 | # (See also functions.mk) 5 | # 6 | # I can put an info/warning/error function as the only entry on a line. I can 7 | # use info/warning as a function returning a null string. 8 | # 9 | # davep 07-Oct-2014 10 | 11 | $(info hello, world) $(info hello, world) 12 | $(info hello, world) $(info hello, world) 13 | 14 | # two lines (info adds carriage return between calls) 15 | $(info Hello, world)$(info Hello, World) 16 | 17 | # resolves to q=42 18 | q$(info assigning to q)=42 19 | $(info q=$q) 20 | 21 | s=$(info hello, world) 22 | $(info info returns s="$s") 23 | 24 | # TODO value() not yet implemented 25 | #a= 26 | #$(info value a=>>$(value a)<<) 27 | #a=42 28 | #$(info value a=>>$(value a)<<) 29 | #a= 30 | #$(if $a,$(error foo),$(info ok)) 31 | #$(if "",$(error foo),$(info ok)) 32 | #$(if ,$(error foo),$(info ok)) 33 | 34 | # should resolve to nothing (leading space is significant) 35 | a=$( info leading space) 36 | $(info a=>$a<) 37 | #$(if $( info leading space),$(error foo),$(info ok)) 38 | 39 | $(info blank line-v) 40 | $(info ) 41 | $(info ^-blank line) 42 | 43 | # fn names in separate namespace than variables so there is significant 44 | # possibilities for confusion. 45 | info=foo 46 | $(info $(info) bar baz) 47 | $(info $(info ) bar baz) 48 | 49 | @:;@: 50 | 51 | -------------------------------------------------------------------------------- /examples/backslash.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | 4 | # simple example showing the virtual line iterator which handles backslashes 5 | # and maintains a file+row+col for every character 6 | # 7 | # See tests/backslash.mk for notes+tests of how GNU Make handles \ 8 | # 9 | # run with: 10 | # PYTHONPATH=. python3 examples/backslash.py 11 | # 12 | # davep 20241116 13 | 14 | import sys 15 | 16 | import pymake.source as source 17 | import pymake.vline as vline 18 | from pymake.scanner import ScannerIterator 19 | from pymake import tokenizer 20 | 21 | def main(infilename): 22 | src = source.SourceFile(infilename) 23 | src.load() 24 | 25 | # iterator across all actual lines of the makefile 26 | line_scanner = ScannerIterator(src.file_lines, src.name) 27 | 28 | # iterator across "virtual" lines which handles the line continuation 29 | # (backslash) 30 | vline_iter = vline.get_vline(src.name, line_scanner) 31 | 32 | # iterate over a file showing single lines joined by backslash 33 | for virt_line in vline_iter: 34 | pos = virt_line.get_pos() 35 | s = str(virt_line) 36 | print(f"{pos} {s}") 37 | 38 | if __name__ == '__main__': 39 | for infilename in sys.argv[1:]: 40 | main(infilename) 41 | else: 42 | print("usage: %s makefile1 [makefile2 [makefile3...]]", file=sys.stderr) 43 | 44 | -------------------------------------------------------------------------------- /examples/showtokens.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | 4 | # simple example showing how to use some of the components of pymake 5 | # 6 | # run with: 7 | # PYTHONPATH=. python3 examples/showtokens.py 8 | # 9 | # davep 20241117 10 | 11 | import sys 12 | import logging 13 | 14 | logger = logging.getLogger("pymake") 15 | 16 | import pymake.source as source 17 | import pymake.vline as vline 18 | from pymake.scanner import ScannerIterator 19 | from pymake import tokenizer 20 | 21 | def main(infilename): 22 | src = source.SourceFile(infilename) 23 | src.load() 24 | 25 | # iterator across all actual lines of the makefile 26 | line_scanner = ScannerIterator(src.file_lines, src.name) 27 | 28 | # iterator across "virtual" lines which handles the line continuation 29 | # (backslash) 30 | vline_iter = vline.get_vline(src.name, line_scanner) 31 | 32 | for virt_line in vline_iter: 33 | s = str(virt_line) 34 | print(s,end="") 35 | vchar_scanner = iter(virt_line) 36 | stmt = tokenizer.tokenize_statement(vchar_scanner) 37 | print(stmt) 38 | 39 | if __name__ == '__main__': 40 | logging.basicConfig(level=logging.INFO) 41 | for infilename in sys.argv[1:]: 42 | main(infilename) 43 | else: 44 | print("usage: %s makefile1 [makefile2 [makefile3...]]", file=sys.stderr) 45 | 46 | 47 | -------------------------------------------------------------------------------- /pymake/printable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # This module completely 100% utterly ignores Unicode. 5 | # Shame on me. 6 | # But it's only used for debugging. 7 | # 8 | 9 | def printable_char(c): 10 | if ord(c) < 32 or ord(c) > 127: 11 | if c == '\t': 12 | return "\\t" 13 | if c == '\n': 14 | return "\\n" 15 | if c == '\r': 16 | return "\\r" 17 | return "\\x{0:02x}".format(ord(c)) 18 | 19 | if c == '\\': 20 | return '\\\\' 21 | # if c == '"': 22 | # return '\\"' 23 | return c 24 | 25 | def printable_string(s): 26 | # Convert a string with unprintable chars and/or weird printing chars into 27 | # something that can be printed without side effects. 28 | # For example, 29 | # -> "\t" 30 | # -> "\n" 31 | # " -> \" 32 | # 33 | # Want to be able to round trip the output of the Symbol hierarchy back 34 | # into valid Python code. 35 | # 36 | return "".join([printable_char(c) for c in s]) 37 | 38 | if __name__ == '__main__': 39 | # showing the difference between '%r' and my fn. 40 | for i in range(256): 41 | print("%r" % chr(i), printable_char(chr(i))) 42 | 43 | s = "".join(chr(i) for i in range(256)) 44 | print(printable_string(s)) 45 | 46 | s = "".join("%r"%chr(i) for i in range(256)) 47 | print(printable_string(s)) 48 | 49 | -------------------------------------------------------------------------------- /examples/virtline.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) David Poole david.poole@ericsson.com 3 | 4 | # simple example showing the virtual line iterator which handles backslashes 5 | # and maintains a file+row+col for every character 6 | # 7 | # run with: 8 | # PYTHONPATH=. python3 examples/virtline.py 9 | # 10 | # davep 20241116 11 | 12 | import sys 13 | import logging 14 | 15 | logger = logging.getLogger("pymake") 16 | 17 | import pymake.source as source 18 | import pymake.vline as vline 19 | from pymake.scanner import ScannerIterator 20 | from pymake import tokenizer 21 | 22 | def main(infilename): 23 | src = source.SourceFile(infilename) 24 | src.load() 25 | 26 | # iterator across all actual lines of the makefile 27 | line_scanner = ScannerIterator(src.file_lines, src.name) 28 | 29 | # iterator across "virtual" lines which handles the line continuation 30 | # (backslash) 31 | vline_iter = vline.get_vline(src.name, line_scanner) 32 | 33 | # iterate over a file showing single lines joined by backslash 34 | for virt_line in vline_iter: 35 | pos = virt_line.get_pos() 36 | s = str(virt_line) 37 | print(f"{pos} {s}") 38 | 39 | if __name__ == '__main__': 40 | logging.basicConfig(level=logging.INFO) 41 | for infilename in sys.argv[1:]: 42 | main(infilename) 43 | else: 44 | print("usage: %s makefile1 [makefile2 [makefile3...]]", file=sys.stderr) 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/test_comment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Test tokenizing (eating) comments. 4 | 5 | import pytest 6 | 7 | from pymake.scanner import ScannerIterator 8 | from pymake.printable import printable_string 9 | from pymake.vline import get_vline 10 | from pymake.source import SourceFile, SourceString 11 | 12 | comments_test_list = ( 13 | # string to test , the expected value after eating the comment 14 | ( "#foo\n", None ), 15 | ( "#\n", None ), 16 | ( "# \nfoo:bar", "foo:bar" ), 17 | ( r"""# this is\ 18 | a run on comment\ 19 | that is annoying\ 20 | and probably a \ 21 | corner case 22 | foo:bar""", "foo:bar" ), 23 | ( r"# I am a comment \\ with two backslashes", None ), 24 | ( r"# I am a comment \ with a backslash", None ), 25 | ) 26 | 27 | @pytest.mark.parametrize("test_tuple", comments_test_list) 28 | def test1(test_tuple): 29 | test_string, expected_result = test_tuple 30 | # print("test={0}".format(printable_string(test_string))) 31 | 32 | src = SourceString( test_string ) 33 | src.load() 34 | scanner = ScannerIterator(src.file_lines, src.name) 35 | vline_iter = get_vline(src.name, scanner) 36 | 37 | try: 38 | oneline = next(vline_iter) 39 | except StopIteration: 40 | oneline = None 41 | 42 | if oneline is None: 43 | assert expected_result is None 44 | else: 45 | assert str(oneline) == expected_result, (str(oneline), expected_result) 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/test_vchar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import logging 4 | import string 5 | import itertools 6 | 7 | import pymake.vline as vline 8 | import pymake.scanner as scanner 9 | 10 | logger = logging.getLogger("pymake.test_vchar") 11 | 12 | def getfile(infilename): 13 | with open(infilename, "r") as infile: 14 | return infile.readlines() 15 | 16 | def test1(): 17 | vs = vline.VCharString() 18 | assert not len(vs) 19 | logging.debug("vs=\"{}\"".format(vs)) 20 | 21 | def test2(): 22 | # read a file, display vchar by vchar 23 | infilename = "tests/test_vchar.py" 24 | 25 | lines = getfile(infilename) 26 | vline_iter = vline.get_vline(infilename, scanner.ScannerIterator(lines, infilename)) 27 | 28 | for virt_line in vline_iter: 29 | for vchar in virt_line: 30 | assert vchar.filename == infilename, vchar.filename 31 | logger.info("%s %s ", vchar, vchar.pos) 32 | 33 | def test3(): 34 | # test string-like append 35 | infilename = "/dev/null" 36 | vs = vline.VCharString() 37 | counter = itertools.count() 38 | 39 | for char, col in zip(string.ascii_letters, counter): 40 | vs += vline.VChar(char, (0, col), infilename) 41 | logger.debug("char=%s col=%d len=%d vs=%s", char, col, len(vs), str(vs)) 42 | 43 | 44 | def main(): 45 | test1() 46 | test2() 47 | test3() 48 | 49 | if __name__=='__main__': 50 | logging.basicConfig(level=logging.DEBUG) 51 | main() 52 | -------------------------------------------------------------------------------- /tests/comments.mk: -------------------------------------------------------------------------------- 1 | # Study various horrible ways comments can be abused 2 | # 3 | # davep 28-sep-2014 4 | 5 | 6 | # this is a comment (duh) 7 | 8 | all : a-rule comment-in-recipe semicolon-then-comment # this is a comment 9 | # this comment ignored by make 10 | # this comment passed to shell 11 | @echo $@ 12 | 13 | #this is a comment\ 14 | with\ 15 | backslashes\ 16 | and\ 17 | should\ 18 | be\ 19 | ignored 20 | 21 | a-rule : a-prereq # a comment 22 | @echo $@ $^ 23 | 24 | # comments comments comments 25 | a-prereq : # comment \ 26 | comment\ 27 | comment\ 28 | comment 29 | @echo $@ $^ 30 | 31 | this-is-a-variable = # this is a comment 32 | $(info = =$(this-is-a-variable)) 33 | 34 | this-is-also-a-variable = # this is a comment\ 35 | that\ 36 | runs\ 37 | along\ 38 | multiple\ 39 | lines 40 | $(info = =$(this-is-also-a-variable)) # empty output 41 | 42 | -include foo.mk # how about comments here? 43 | 44 | comment-in-recipe : 45 | @echo $@ 46 | # this is a Makefile commment 47 | @echo after makefile comment # this is a shell comment 48 | @echo this line is also passed to the shell \ 49 | including the continuation \ 50 | # and this comment 51 | # this\ 52 | is\ 53 | a\ 54 | makefile\ 55 | comment 56 | @echo after the big long comment 57 | # this is also a makefile comment 58 | @echo after another makefile comment 59 | end-of-recipe = 42 # end of recipe 60 | $(info = 42=$(end of recipe)) 61 | 62 | semicolon-then-comment : ; # semicolon comment should be passed to the shell 63 | 64 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Whitebox testing of function implementation internals. 3 | # 4 | # TODO add moar tests 5 | 6 | import logging 7 | 8 | logger = logging.getLogger("pymake") 9 | logging.basicConfig(level=logging.DEBUG) 10 | 11 | from pymake.error import * 12 | from pymake.symbol import * 13 | import pymake.functions_str as functions_str 14 | import pymake.symtable as symtable 15 | 16 | # turn on internal behaviors that allow us to create literals without VCharString 17 | import pymake.symbol as symbol 18 | symbol._testing = True 19 | 20 | def test1(): 21 | symbol_table = symtable.SymbolTable() 22 | expr = Literal('1,foo bar baz qux') 23 | 24 | word_fn = functions_str.Word( [expr] ) 25 | result = word_fn.eval(symbol_table) 26 | assert result=="foo" 27 | 28 | def test_word_invalid_index(): 29 | symbol_table = symtable.SymbolTable() 30 | expr = Literal('q,foo bar baz qux') 31 | 32 | word_fn = functions_str.Word( [expr] ) 33 | try: 34 | result = word_fn.eval(symbol_table) 35 | except InvalidFunctionArguments as err: 36 | pass 37 | else: 38 | assert 0, "should have failed" 39 | 40 | def test_word_bad_index(): 41 | symbol_table = symtable.SymbolTable() 42 | expr = Literal('-1,foo bar baz qux') 43 | 44 | word_fn = functions_str.Word( [expr] ) 45 | try: 46 | result = word_fn.eval(symbol_table) 47 | except InvalidFunctionArguments as err: 48 | pass 49 | else: 50 | assert 0, "should have failed" 51 | 52 | -------------------------------------------------------------------------------- /tests/conditional.mk: -------------------------------------------------------------------------------- 1 | out=(if a,b,c,d,e,f,g,h,i,j) 2 | $(info out=$(out)) 3 | out=$(if a,b,c,d,e,f,g,h,i,j) 4 | $(info out=$(out)) 5 | out=$(if a b c,d e f,g h i j) 6 | $(info out=$(out)) 7 | 8 | a=1 9 | b=2 10 | c=3 11 | d=4 12 | e=5 13 | f=6 14 | g=7 15 | h=8 16 | out=$(if $a,$b,$c,$d,$e,$f) 17 | $(info out1=$(out)) 18 | a=# 19 | out=$(if $a,$b,$c,$d,$e,$f) 20 | $(info out2=$(out)) 21 | 22 | # gnu make ignores extra params 23 | out=$(if a,b,c,d,e,f,g,h) 24 | $(info out3a=$(out)) 25 | out=$(if $a,b,c,d,e, f, g, $h) 26 | $(info out3b=$(out)) 27 | 28 | # 'if's 3rd arg is optional 29 | a=1 30 | foo=$(if $a,$b) 31 | $(info 3rd arg foo1=$(foo)) 32 | a=# 33 | foo=$(if $a,$b) 34 | $(info 3rd arg foo2=>>$(foo)<<) 35 | 36 | foo=bar 37 | qux=$(if $(foo),$(info foo=$(foo)),$(info nofoo4u)) 38 | $(info qux=$(qux)) # should be empty 39 | 40 | # 41 | # OR 42 | # 43 | a=1 44 | foo=$(or $a$a,$b,$c) 45 | $(info or1-foo=$(foo)) 46 | a=# 47 | foo=$(or $a,$b,$c) 48 | $(info or2-foo=$(foo)) 49 | foo=$(or $a,$b,$c,$d,$e,$f,$g,$h) 50 | $(info or3-foo=$(foo)) 51 | b=# 52 | foo=$(or $a,$b,$c,$d,$e,$f,$g,$h) 53 | $(info or4-foo=$(foo)) 54 | foo=$(or $a$b,$c,$d,$e,$f,$g,$h) 55 | $(info or5-foo=$(foo)) 56 | a=1 57 | b=2 58 | c=3 59 | foo=$(or $a $b $c $d $e $f $g $h) 60 | $(info or6-foo=$(foo)) 61 | 62 | blank=# 63 | space = ${blank} ${blank} 64 | $(info space=>>${space}<<) 65 | foo = $(or ${blank},${space},qqq) 66 | $(info foo3=$(foo)) 67 | 68 | # 69 | # AND 70 | # 71 | a=1 72 | b=2 73 | 74 | foo=$(and a,b,c,d) 75 | $(info and1-foo=$(foo)) 76 | 77 | 78 | @:;@: 79 | -------------------------------------------------------------------------------- /tests/test_iter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # test vline_iter and line_iter stay in sync 4 | 5 | import sys 6 | from pymake.vline import get_vline 7 | from pymake.scanner import ScannerIterator 8 | import random 9 | 10 | def iter_test(file_lines): 11 | filename = "/dev/null" 12 | line_iter = ScannerIterator(file_lines, filename) 13 | vline_iter = get_vline(filename, line_iter) 14 | 15 | while 1: 16 | if random.choice((0,1)): 17 | s = next(line_iter) 18 | else : 19 | s = next(vline_iter) 20 | yield s 21 | 22 | def iter_test_filename(infilename): 23 | with open(infilename,'r') as infile : 24 | # tortuous filter to kill comment lines and empty lines or the test 25 | # will fail because vline eats those 26 | file_lines = [ l for l in infile.readlines() if 27 | not l.lstrip().startswith("#") and 28 | len(l.strip()) > 0 ] 29 | 30 | # print(file_lines) 31 | file_lines_out = list(iter_test(file_lines)) 32 | # print([str(s) for s in file_lines_out]) 33 | 34 | for l,r in zip(file_lines,file_lines_out): 35 | assert l==str(r),(l,str(r)) 36 | 37 | def iter_test_range(): 38 | nums = [ str(n) for n in range(100) ] 39 | nums_out = list(iter_test(nums)) 40 | 41 | for l,r in zip(nums,nums_out): 42 | assert l==str(r),(l,str(r)) 43 | 44 | if __name__=='__main__': 45 | iter_test_range() 46 | 47 | infilename = sys.argv[1] 48 | iter_test_filename(infilename) 49 | 50 | -------------------------------------------------------------------------------- /tests/balanced-parens.mk: -------------------------------------------------------------------------------- 1 | # GNU Make wants paren/brackets balanced even if not in an actual fn call. 2 | # The open char needs to match the close char ( match ) { match }. 3 | # Opposite char of ( vs { is ignored and can be unbalanced. 4 | 5 | a,b = 42 6 | $(info $(a,b),$(a,b)) 7 | $(info $$(a,b),$$(a,b)) 8 | 9 | # literal dollar: parens must balance 10 | $(info $$(())) 11 | ${info $${{}}} 12 | 13 | # no need to balance (s because using { 14 | ${info t=$$(())} 15 | ${info t=$$((} 16 | 17 | # nested: must balance 18 | ${info u=$(info uu=())} 19 | 20 | # extra closing ) outside the () but inside {} so ok 21 | ${info u=$(info uu=()))} 22 | # ^-- extra closing ) 23 | 24 | # unbalanced opening ( ok because inside {} 25 | ${info v=$$(()} 26 | 27 | # mismatched open/close 28 | # unterminated call to function 'info': missing ')' 29 | #$(info v=$$(()} 30 | 31 | # unterminated call to function 'info': missing ')' 32 | #$(info $$(()) 33 | 34 | $(info w=$${{{{{) 35 | 36 | # balanced { } with ignored ) inside 37 | ${info x=$${{{{{)}}}}}} 38 | 39 | # unterminated call to function 'info': missing ')' 40 | #$(info $$((((() 41 | 42 | # unterminated call to function 'info': missing ')' 43 | #$(info $((((() 44 | 45 | # missing separator 46 | #${info foo}} 47 | 48 | # missing separator 49 | #${info foo}{ 50 | 51 | # missing separator 52 | #${info foo}q 53 | 54 | # I don't know why I did this but it's amusing 55 | $(info a$(info b$(info c$(info d$(info e$(info f$info)))))) 56 | $(info $$(info $$(info $$(info $$(info $$(info $$info)))))) 57 | 58 | @:;@: 59 | 60 | -------------------------------------------------------------------------------- /tests/test_whitespace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | import run 5 | import verify 6 | 7 | def test_simple_whitespace(): 8 | makefile=""" 9 | # leading whitespace is discarded 10 | # trailing whitespace is preserved (there are three spaces after 'BAR') 11 | FOO:= BAR 12 | $(info FOO=>>>$(FOO)<<<) 13 | @:;@: 14 | """ 15 | p = run.gnumake_string(makefile) 16 | print("p=",p) 17 | assert p=="FOO=>>>BAR <<<" 18 | 19 | p = run.pymake_string(makefile) 20 | print("p=",p) 21 | assert p=="FOO=>>>BAR <<<" 22 | 23 | def test_simple_whitespace_tabs(): 24 | makefile=""" 25 | # leading whitespace is discarded 26 | # trailing whitespace is preserved (there are three tabsafter 'BAR') 27 | FOO:= BAR 28 | $(info FOO=>>>$(FOO)<<<) 29 | @:;@: 30 | """ 31 | p = run.gnumake_string(makefile) 32 | print("p=",p) 33 | assert p=="FOO=>>>BAR <<<" 34 | 35 | p = run.pymake_string(makefile) 36 | print("p=",p) 37 | assert p=="FOO=>>>BAR <<<" 38 | 39 | def test_whitespace_SHELL(): 40 | # whitespace after /bin/sh 41 | makefile=""" 42 | SHELL:=/bin/sh 43 | $(info SHELL=>>>$(SHELL)<<<) 44 | $(info $(shell echo hello, world)) 45 | @:;@: 46 | """ 47 | expect = ( 48 | "SHELL=>>>/bin/sh <<<", 49 | "hello, world", 50 | ) 51 | 52 | p = run.gnumake_string(makefile) 53 | print("p=",p) 54 | verify.compare_result_stdout(expect, p) 55 | 56 | p = run.pymake_string(makefile) 57 | print("p=",p) 58 | verify.compare_result_stdout(expect, p) 59 | 60 | -------------------------------------------------------------------------------- /tests/wordlist.mk: -------------------------------------------------------------------------------- 1 | 2 | $(info 1 $(wordlist 2, 3, foo bar baz)) 3 | $(info 1 $(wordlist 1, 3, foo bar baz)) 4 | 5 | # *** invalid first argument to 'wordlist' function: '0'. Stop. 6 | #$(info 1 $(wordlist 0, 3, foo bar baz)) 7 | 8 | # start > end 9 | $(info 2 $(wordlist 3, 2, foo bar baz)) # empty 10 | 11 | # empty but /tmp/tmp.txt exists showing GNU make doesn't short circuit the 12 | # expression 13 | $(info 2 $(wordlist 3, 2, $(shell touch /tmp/tmp.txt))) 14 | 15 | x:=aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz 16 | $(info 3 $(wordlist 1, 2, $(x))) 17 | $(info 4 $(wordlist 20, 30, $(x))) 18 | $(info 5 $(wordlist 99, 9999, $(x))) 19 | 20 | x:= aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz # trailing spaces 21 | $(info 3b >>$(wordlist 1, 2, $(x))<<) 22 | $(info 4b >>$(wordlist 20, 30, $(x))<<) 23 | $(info 5b $(wordlist 99, 9999, $(x))) 24 | 25 | $(info empty=$(wordlist 1,10,$(empty))) 26 | 27 | x:= aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz # trailing spaces 28 | $(info tab1 >>$(wordlist 1, 2, $(x))<<) 29 | $(info tab2 >>$(wordlist 20, 30, $(x))<<) 30 | $(info tab3 $(wordlist 99, 9999, $(x))) 31 | 32 | # string too short for any match 33 | $(info exceed=$(wordlist 8,10,aa bb cc)) 34 | 35 | $(info one=$(wordlist 1,10,aaaaaaaaaaaa)) 36 | $(info one=$(wordlist 2,10,aaaaaaaaaaaa)) 37 | 38 | $(info one=$(wordlist 1,0,aa aa aa aaa)) 39 | 40 | $(info files=$(wordlist 1,3,$(sort $(wildcard *.py)))) 41 | 42 | @:;@: 43 | 44 | -------------------------------------------------------------------------------- /tests/multiline.mk: -------------------------------------------------------------------------------- 1 | bar=bar 2 | # two-lines is from the GNU Make manual 3 | define two-lines 4 | @echo two-lines foo 5 | @echo two-lines $(bar) 6 | endef 7 | 8 | define echo-stuff 9 | echo foo 10 | echo bar 11 | echo baz 12 | endef 13 | 14 | # the extra leading tabs/spaces are all eaten 15 | define multi-line-define 16 | @echo this\ 17 | is\ 18 | a\ 19 | multi-line\ 20 | define 21 | endef 22 | 23 | # This macro could be an error because the trailing spaces after the backslash 24 | # breaks the macro being a multi-line. If the \ is incorrectly treated 25 | # as a continuation, then the entire output will contain the extra "echo" 26 | # strings. When the \ is properly ignored, the block will be multiple 27 | # separate shell calls. 28 | # 29 | define trailing-spaces-define 30 | @echo this\ 31 | echo line \ 32 | echo has \ 33 | echo spaces \ 34 | echo after \ 35 | echo the backslashes 36 | @echo for shame 37 | endef 38 | 39 | define mixed-backslashes 40 | @echo this\ 41 | line\ 42 | is\ 43 | backslashed 44 | @echo this line is not 45 | @echo but\ 46 | this\ 47 | line\ 48 | is\ 49 | again\ 50 | backslashed 51 | endef 52 | 53 | all: test1 test2 test3 test4 test5 test6 test7 54 | 55 | test1: 56 | echo this\ 57 | is\ 58 | a\ 59 | shell\ 60 | continuation 61 | 62 | test2: 63 | $(two-lines) 64 | 65 | test3: 66 | $(echo-stuff) 67 | 68 | test4: 69 | echo this 70 | echo is 71 | echo multiple 72 | echo lines 73 | 74 | test5: 75 | $(multi-line-define) 76 | 77 | test6: 78 | $(trailing-spaces-define) 79 | 80 | test7: 81 | $(mixed-backslashes) 82 | -------------------------------------------------------------------------------- /tests/test_sexpr.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import logging 4 | 5 | logger = logging.getLogger("pymake") 6 | #logging.basicConfig(level=logging.DEBUG) 7 | 8 | from pymake.symbol import * 9 | from pymake.functions_str import * 10 | from pymake.symtable import SymbolTable 11 | 12 | # turn on internal behaviors that allow us to create literals without VCharString 13 | import pymake.symbol as symbol 14 | symbol._testing = True 15 | 16 | def test1(): 17 | # test some S Expression execution 18 | # 19 | symtable = SymbolTable() 20 | symtable.add("target", ["abcdefghijklmnopqrstuvwxyz"]) 21 | 22 | a = AssignmentExpression([Expression([Literal("UPPERCASE")]), AssignOp("="), Expression([Subst([Literal("z,Z,"), Subst([Literal("y,Y,"), Subst([Literal("x,X,"), Subst([Literal("w,W,"), Subst([Literal("v,V,"), Subst([Literal("u,U,"), Subst([Literal("t,T,"), Subst([Literal("s,S,"), Subst([Literal("r,R,"), Subst([Literal("q,Q,"), Subst([Literal("p,P,"), Subst([Literal("o,O,"), Subst([Literal("n,N,"), Subst([Literal("m,M,"), Subst([Literal("l,L,"), Subst([Literal("k,K,"), Subst([Literal("j,J,"), Subst([Literal("i,I,"), Subst([Literal("h,H,"), Subst([Literal("g,G,"), Subst([Literal("f,F,"), Subst([Literal("e,E,"), Subst([Literal("d,D,"), Subst([Literal("c,C,"), Subst([Literal("b,B,"), Subst([Literal("a,A,"), VarRef([Literal("target")])])])])])])])])])])])])])])])])])])])])])])])])])])])])]) 23 | 24 | print(a) 25 | a.eval(symtable) 26 | print(a.makefile()) 27 | 28 | result = symtable.fetch('target') 29 | print(f"result={result}") 30 | assert result[0]=='abcdefghijklmnopqrstuvwxyz' 31 | 32 | if __name__ == '__main__': 33 | test1() 34 | 35 | -------------------------------------------------------------------------------- /tests/shell.mk: -------------------------------------------------------------------------------- 1 | # Tinker with GNU Make $(shell) function 2 | # davep 19-Sep-2014 3 | 4 | # can't use any commands that produce different output (such as date) 5 | 6 | four != expr 2 + 2 7 | $(info four=$(four)) 8 | 9 | help != expr --help 10 | status=$(.SHELLSTATUS) 11 | $(info help=$(words $(help)) status=$(status)) 12 | 13 | # FIXME pymake doesn't yet recognize $(file) varref vs $(file) function) 14 | #file=hello.c 15 | #$(info $(file)) 16 | 17 | filename=tests/hello.c 18 | includes= $(shell cat $(filename) | grep includes) 19 | $(info includes=$(includes)) 20 | 21 | filename=mulmer.c 22 | $(info includes=$(include)) 23 | 24 | foo = $(shell echo foo) 25 | $(info foo=$(foo)) 26 | 27 | pyfiles = $(shell echo *.py) 28 | $(info pyfiles=$(pyfiles)) 29 | 30 | pyfiles = $(shell ls *.py) 31 | $(info pyfiles=$(pyfiles)) 32 | 33 | # quotes find their way into the shell cmd 34 | # ls: cannot access '*.py': No such file or directory 35 | foo = $(shell ls '*.py') 36 | $(info foo=$(foo)) 37 | 38 | # no such file or directory 39 | fail= $(shell abcdefghijklmnopqrstuvwxyz) 40 | status:=$(.SHELLSTATUS) 41 | $(info fail=$(fail) status=$(status)) 42 | 43 | # Permission denied 44 | # FIXME not portable 45 | fail= $(shell /etc/shadow) 46 | status:=$(.SHELLSTATUS) 47 | $(info fail=$(fail) status=$(status)) 48 | 49 | self=$(shell head -1 shell.mk) 50 | $(info self=$(self)) 51 | 52 | exit:=$(shell echo I shall fail now && exit 1) 53 | $(info exit=$(exit) status=$(.SHELLSTATUS)) 54 | 55 | exit:=$(shell exit 42) 56 | $(info exit=$(exit) status=$(.SHELLSTATUS)) 57 | 58 | # exit status should be that of last command executed 59 | exit:=$(shell echo foo && exit) 60 | $(info exit=$(exit) status=$(.SHELLSTATUS)) 61 | 62 | @:;@: 63 | 64 | -------------------------------------------------------------------------------- /tests/test_scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Finally writing a regression test for ScannerIterator. 4 | # 5 | # davep 16-Nov-2014 6 | 7 | from pymake.scanner import ScannerIterator 8 | from pymake.vline import VChar, VirtualLine 9 | 10 | def test1() : 11 | input_str = "hello, world" 12 | input_iter = iter(input_str) 13 | s = ScannerIterator( input_str, "/dev/null" ) 14 | for c in s: 15 | # print(c,end="") 16 | input_c = next(input_iter) 17 | assert c==input_c, (c,input_c) 18 | # print() 19 | 20 | def test_pushback(): 21 | s = ScannerIterator("hello, world", "/dev/null" ) 22 | assert s.next()=='h' 23 | assert s.next()=='e' 24 | s.pushback() 25 | s.pushback() 26 | assert s.next()=='h' 27 | assert s.next()=='e' 28 | s.pushback() 29 | s.pushback() 30 | assert s.lookahead()=='h' 31 | assert s.next()=='h' 32 | assert s.next()=='e' 33 | 34 | def test_state_push_pop(): 35 | s = ScannerIterator("hello, world", "/dev/null" ) 36 | s.push_state() 37 | for c in s : 38 | if c==' ': 39 | break 40 | assert s.remain()=="world" 41 | s.pop_state() 42 | assert s.remain()=='hello, world', s.remain() 43 | 44 | def test_lookahead(): 45 | s = ScannerIterator("hello, world", "/dev/null" ) 46 | assert s.lookahead() == 'h' 47 | next(s) 48 | assert s.lookahead() == 'e' 49 | assert s.remain() == "ello, world" 50 | 51 | # 20230101 I don't know if I need peek_back() anymore 52 | #def test_peek_back(): 53 | # s = ScannerIterator("hello, world", "/dev/null" ) 54 | # assert next(s) == 'h' 55 | # assert next(s) == 'e' 56 | # assert s.peek_back() == 'e' 57 | 58 | if __name__=='__main__': 59 | main() 60 | 61 | -------------------------------------------------------------------------------- /pymake/html.py: -------------------------------------------------------------------------------- 1 | # https://perfectmotherfuckingwebsite.com/ 2 | # copyrighted https://creativecommons.org/publicdomain/zero/1.0/ 3 | # 4 | css = """ 5 | body{max-width:650px;margin:40px auto;padding:0 10px;font:18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";color:#444}h1,h2,h3{line-height:1.2}@media (prefers-color-scheme: dark){body{color:#c9d1d9;background:#0d1117}a:link{color:#58a6ff}a:visited{color:#8e96f0}} 6 | """ 7 | 8 | top=""" 9 | 10 | 11 | 12 | 13 | Makefile 14 | 15 | 16 | 17 | 18 | 19 | 20 | """ 21 | 22 | bottom=""" 23 | 24 | """ 25 | 26 | def p(s): 27 | return "

%s

\n" % s 28 | 29 | def _save_rules(outfile, rules): 30 | for target,rule in rules.items(): 31 | outfile.write(p(target + " : " + " ".join([pr for pr in rule.prereq_list if not pr.startswith("/usr")]))) 32 | 33 | def save_rules(outfilename, rules): 34 | with open("style.css","w") as outfile: 35 | outfile.write(css) 36 | 37 | with open(outfilename,"w") as outfile: 38 | outfile.write(top) 39 | _save_rules(outfile,rules) 40 | outfile.write(bottom) 41 | 42 | -------------------------------------------------------------------------------- /tests/include.mk: -------------------------------------------------------------------------------- 1 | rulefile:=$(wildcard smallest.mk) 2 | ifeq (${rulefile},) 3 | # didn't find in the current directory; let's try under our tests 4 | rulefile:=$(wildcard tests/smallest.mk) 5 | ifeq (${rulefile},) 6 | $(error unable to find smallest.mk) 7 | endif 8 | endif 9 | $(info will include $(rulefile) for rules) 10 | 11 | include $(rulefile) 12 | -include noname.mk 13 | sinclude noname.mk 14 | 15 | # this creates a variable named 'include' 16 | # (doesn't include a file named '=') 17 | include = foo bar baz 18 | ifndef include 19 | $(error should have been a variable) 20 | endif 21 | $(info include=$(include)) 22 | 23 | # an assignment expression 24 | include=foo bar baz 25 | ifndef include 26 | $(error include required) 27 | endif 28 | $(info include=$(include)) 29 | ifneq ($(include),foo bar baz) 30 | $(error include should have been foo bar baz) 31 | endif 32 | 33 | # bare include is not an error; seems to be ignored 34 | include 35 | 36 | filename=noname.mk 37 | -include $(filename) 38 | 39 | # parses to a rule "include a" with target =b ??? 40 | #include a+=b 41 | 42 | # What in the holy hell. Make hitting implicit catch-all for missing include 43 | # names? WTF? 44 | # 45 | # TODO 46 | # "Once it has finished reading makefiles, make will try to remake any that are 47 | # out of date or don’t exist. See Section 3.5 [How Makefiles Are Remade], page 48 | # 14. Only after it has tried to find a way to remake a makefile and failed, 49 | # will make diagnose the missing makefile as a fatal error." 50 | # 51 | # 3.81 52 | # {implicit} noname.mk 53 | # {implicit} baz 54 | # {implicit} bar 55 | # {implicit} foo 56 | # {implicit} = 57 | # 58 | # Newer versions' behavior changed. 59 | # 60 | # 3.82 61 | # {implicit} noname.mk 62 | # 63 | # 4.0 64 | # {implicit} noname.mk 65 | 66 | #% : ; @echo {implicit} $@ 67 | 68 | -------------------------------------------------------------------------------- /tests/findstring.mk: -------------------------------------------------------------------------------- 1 | $(info $(findstring a,a b c)) 2 | $(info $(findstring a, b c)) 3 | 4 | s=foo bar baz 5 | $(info $(findstring foo, $s)) 6 | $(info $(findstring foo, a b c $s d e f g)) 7 | 8 | $(info $(findstring foo, foo bar baz)) 9 | $(info $(findstring qux, foo bar baz)) 10 | 11 | s:=foo bar baz 12 | $(info $(findstring foo, $s)) 13 | $(info $(findstring foo, a b c $s d e f g)) 14 | 15 | s=foo bar baz 16 | $(info $(findstring foo, $s)) 17 | $(info $(findstring foo, a b c $s d e f g)) 18 | x=a b c d e f g 19 | $(info 1 a=$(findstring a,$x)) 20 | $(info 2 a=$(findstring a, $x )) 21 | $(info 3 a=$(findstring a,$x $x $x)) # single a (no duplicates) 22 | $(info 4 a=$(findstring a,$x$x$x)) # single a (no duplicates) 23 | 24 | $(info a b=$(findstring a b,$x)) # a b ; whitespace in "find" param is preserved as part of the string 25 | $(info a b=$(findstring a b,$x)) # empty (whitespace mismatch) 26 | $(info a b=$(findstring a b, a b c d e f g )) # a b (whitespace match) 27 | 28 | $(info blank=>>$(findstring a b q,$x)<<) # >><< (blank) 29 | $(info notblank=>>$(findstring a b q,$(subst c,q,$x))<<) # >>a b q<< 30 | 31 | $(info 1 the=$(findstring the,hello there all you rabbits)) # the (partial string match is valid) 32 | $(info 2 the=$(findstring the,now is the time for all good men to come to the aid of their country)) # the (only single match) 33 | 34 | t=t 35 | h=h 36 | e=e 37 | $(info 3 the=$(findstring $t$h$e,now is the time for all good men to come to the aid of their country)) # the (only single match) 38 | $(info 4 the=$(findstring $t$h$e,now is the time for all good men to come to the aid of their country)) # the (only single match) 39 | # case insensitve but will find 'the' in 'their' 40 | $(info 5 the=$(findstring $t$h$e,now is THE time for all good men to come to THE aid of their country)) # the (only single match) 41 | 42 | @:;@: 43 | 44 | -------------------------------------------------------------------------------- /tests/functions_str.mk: -------------------------------------------------------------------------------- 1 | # functions that operate on strings 2 | 3 | # from the GNU make manual 4 | comma:= , 5 | empty:= 6 | space:= $(empty) $(empty) 7 | 8 | # 9 | # SORT 10 | # 11 | # Sort does not take arguments function-style (commas are not interpreted as 12 | # separate arguments. Rather then entire single arg is interpretted as a space 13 | # separated list. 14 | 15 | # make sorts _textually_ not numerically so this will show up as 1 10 2 3 4 ... 16 | a=10 9 8 7 6 5 4 3 2 1 17 | x=$(sort $(a)) 18 | $(info a sort=$(x)) 19 | 20 | duplicates=1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 21 | $(info dups sort=$(sort $(duplicates))) 22 | 23 | b=e d c b a 24 | c=8 7 6 5 3 0 9 25 | $(info sortme=$a,$b,$c) 26 | x=$(sort $a,$b,$c) 27 | $(info sort=$(x)) 28 | 29 | # result 6,5,4,3,2,1 30 | a=6 31 | b=5 32 | c=4 33 | d=3 34 | e=2 35 | f=1 36 | x=$(sort $a,$b,$c,$d,$e,$f) 37 | $(info abc sort=$(x)) 38 | 39 | # result 66,55,44,33,22,11 40 | x=$(sort $a$a,$b$b,$c$c,$d$d,$e$e,$f$f) 41 | $(info aabbcc sort=$(x)) 42 | 43 | # result 1 2,1 3,2 4,3 5,4 6,5 44 | # result 66,55,44,33,22,11 45 | x=$(sort $a $a,$b $b,$c $c,$d $d,$e $e,$f $f) 46 | $(info a ab bc c sort=$(x)) 47 | 48 | # what about extra spaces hidden? when does split happen vs variable 49 | # substitution? should see "4 5 6" 50 | x = $(sort $a${space}$b${space}$c${space}) 51 | $(info spaces abc sort=>>$(x)<<) 52 | 53 | x = $(sort $a${space}${space}$b${space}${space}$c${space}${space}) 54 | $(info spaces2 abc sort=>>$(x)<<) 55 | 56 | x = $(sort $a $b $c ) 57 | $(info spaces3 abc sort=>>$(x)<<) 58 | 59 | # tabs 60 | x = $(sort $a $b $c ) 61 | $(info spaces3 abc sort=>>$(x)<<) 62 | 63 | a := $(findstring a,a b c) 64 | $(info a=$(a)) 65 | 66 | a := $(filter a,a a a a a a a a a a a b) 67 | $(info a=$(a)) 68 | 69 | sources := foo.c bar.c baz.s ugh.h 70 | $(info cc $(filter %.c %.s,$(sources)) -o foo) 71 | 72 | @:;@: 73 | 74 | -------------------------------------------------------------------------------- /pymake/submake.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | # 4 | # handle creation of the py-submake helper script. 5 | 6 | # In order to maintain the debugger state in a single process (once I write the 7 | # debugger, of course), I run all sub-makes in the same process as the main 8 | # pymake. 9 | # 10 | # However, I need the $SHELL to interpret the commands passed. In a trival example: 11 | # 12 | # all: 13 | # $(MAKE) -C $$PWD/subdir $$HOME 14 | # 15 | # The $$PWD becomes $PWD sent to the shell. The env var is substituted by the shell 16 | # then passed as as argv to my helper. When the helper finishes, the stdout of 17 | # the subprocess is parsed to find the actual arguments passed to the sub-make. 18 | # 19 | 20 | import os 21 | import os.path 22 | 23 | submake_helper="""\ 24 | #!/bin/sh 25 | 26 | set -eu 27 | 28 | # output all elements of argv on own line so we can split() the shell 29 | # interpretted args on \\n 30 | 31 | echo $0 32 | for a in $@ ; do 33 | echo $a 34 | done 35 | """ 36 | import atexit 37 | 38 | def getname(): 39 | return os.path.join( os.getcwd(), "py-submake-%d" % os.getpid() ) 40 | 41 | def create_helper(): 42 | outfilename = getname() 43 | if os.path.exists(outfilename): 44 | return outfilename 45 | # create an executable file 46 | # TODO windows batch file??? 47 | fd = os.open(outfilename, os.O_CREAT|os.O_WRONLY, mode=0o755) 48 | os.write(fd, submake_helper.encode("utf8") ) 49 | os.close(fd) 50 | 51 | atexit.register(remove_helper) 52 | return outfilename 53 | 54 | def remove_helper(): 55 | # clean up after myself 56 | # should be safe because I'm only ever running with the one main process (no threads) 57 | try: 58 | os.unlink( getname() ) 59 | except OSError: 60 | pass 61 | 62 | if __name__ == '__main__': 63 | print(create_helper()) 64 | 65 | -------------------------------------------------------------------------------- /tests/test_ifdef.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | 3 | import pytest 4 | 5 | import run 6 | 7 | # run both make and pymake 8 | # these ifdef tests are simple enough for a pass/fail 9 | def run_test(makefile): 10 | run.simple_test(makefile) 11 | 12 | def test_ifdef_mixed_build(): 13 | # If the value of that variable has a non-empty value, the text-if-true 14 | # is effective; otherwise, the text-if-false, if any, is effective. Variables 15 | # that have never been defined have an empty value." 16 | # 17 | # My bug discovered while parsing linux kernel makefile. A var must exist 18 | # AND be non-empty for ifdef to eval true. 19 | s = """ 20 | mixed-build:= 21 | ifdef mixed-build 22 | $(error should not see this) 23 | endif 24 | @:;@: 25 | """ 26 | run_test(s) 27 | 28 | def test_ifdef_simple(): 29 | s = """ 30 | foo:=1 31 | ifdef foo 32 | else 33 | $(error should have found foo) 34 | endif 35 | @:;@: 36 | """ 37 | run.pymake_string(s) 38 | 39 | def test_ifndef_simple(): 40 | s = """ 41 | foo:=1 42 | ifndef foo 43 | $(error should have found foo) 44 | endif 45 | @:;@: 46 | """ 47 | run.pymake_string(s) 48 | 49 | def test_ifdef_empty(): 50 | s = """ 51 | foo:= 52 | ifdef foo 53 | $(error should not have found foo) 54 | endif 55 | @:;@: 56 | """ 57 | run.pymake_string(s) 58 | 59 | def test_ifdef_trailing_whitespace(): 60 | # six spaces after 'foo:=' 61 | s = """ 62 | foo:= 63 | ifdef foo 64 | $(error should not have found foo) 65 | endif 66 | @:;@: 67 | """ 68 | run.pymake_string(s) 69 | 70 | def test_ifdef_trailing_tabs(): 71 | s = """ 72 | foo:= 73 | ifdef foo 74 | $(error should not have found foo) 75 | endif 76 | @:;@: 77 | """ 78 | run.pymake_string(s) 79 | 80 | 81 | def test_ifdef_empty_recursive_assign(): 82 | s = """ 83 | foo= 84 | ifdef foo 85 | $(error should not have found foo) 86 | endif 87 | @:;@: 88 | """ 89 | run.pymake_string(s) 90 | 91 | -------------------------------------------------------------------------------- /tests/export.mk: -------------------------------------------------------------------------------- 1 | export#foofoofoo 2 | 3 | $(info FOO=$(FOO)) 4 | 5 | # is this valid? what does this do? 6 | # I think it exports var "FOO CC" value "gcc" (NOPE) 7 | # export FOO and export CC=gcc? that would make more sense 8 | # Wild. It's creating FOO and CC=gcc *but only as env vars* 9 | # (printenv recipe below sees FOO 10 | export FOO CC=gcc 11 | 12 | v=FOO CC 13 | ifndef v 14 | $(error v) 15 | endif 16 | $(info v=$(v)) 17 | $(info $$v=$($(v))) # nothing 18 | 19 | ifdef FOO 20 | $(info FOO exists) 21 | endif 22 | 23 | ifdef CC 24 | $(CC=$(CC)) 25 | endif 26 | 27 | export f#foofoofoo 28 | export f #foofoofoo 29 | 30 | # export expression (note the backslash that makes my life diffcult) 31 | export\ 32 | CC=clang 33 | 34 | # bare export says "export everything by default" 35 | export 36 | 37 | unexport 38 | export A B C D E F G 39 | unexport A B C D E F G 40 | 41 | CC=icc 42 | LD=ld 43 | RM=rm 44 | export CC LD RM 45 | export $(CC) $(LD) $(RM) 46 | 47 | # multiple directives? 48 | # nope. Looks like only export only allows expression on RHS 49 | export override LD=ld 50 | $(info override LD=ld) 51 | #ifneq ("$(override LD)","ld") 52 | #$(error export override error) 53 | #endif 54 | 55 | # what does make do here? 56 | export export CFLAGS=CFLAGS 57 | $(info weird=$(export CFLAGS)) 58 | 59 | # make 3.81 "export define" not allowed ("missing separator") 60 | # make 3.82 works 61 | # make 4.0 works 62 | export define foo 63 | foo says bar 64 | foo says bar again 65 | endef 66 | $(info $(call foo)) 67 | 68 | export foo 69 | 70 | export\ 71 | a:=b 72 | $(info a=$(a)) 73 | 74 | export CC:=gcc 75 | export CFLAGS+=-Wall 76 | 77 | # creates an env var named "CC" with value "gcc CFLAGS=-Wall" ? 78 | export CC=gcc CFLAGS=-Wall 79 | ifneq ($(CC),gcc CFLAGS=-Wall) 80 | $(error fail) 81 | endif 82 | 83 | # printenv will exit non-zero if value not in environment 84 | test: 85 | printenv CC 86 | printenv CFLAGS 87 | printenv foo 88 | printenv gcc 89 | printenv ld 90 | printenv rm 91 | printenv FOO 92 | 93 | -------------------------------------------------------------------------------- /pymake/makedb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Gather GNU Make's internal database by running `make -p` and parsing the 4 | # results, adding vars, rules, to myself. 5 | # 6 | # NOTE! This very likely makes my code under the same license as GNU Make (GPLv3). 7 | # TODO probably need to update my LICENSE and COPYING and etc. 8 | # 9 | # TODO before releasing this project to broad distribution, need to figure out how 10 | # the licensing will work. 11 | # 12 | # As of 20221002, I get the following with the database dump: 13 | ###### -- snip 14 | # GNU Make 4.3 15 | # Built for x86_64-redhat-linux-gnu 16 | # Copyright (C) 1988-2020 Free Software Foundation, Inc. 17 | # License GPLv3+: GNU GPL version 3 or later 18 | # This is free software: you are free to change and redistribute it. 19 | # There is NO WARRANTY, to the extent permitted by law. 20 | ###### -- snip 21 | 22 | import subprocess 23 | 24 | def run_make(cmdline): 25 | cmd = cmdline.split() 26 | p = subprocess.run(cmd, 27 | shell=False, 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.PIPE, 30 | universal_newlines=True 31 | ) 32 | 33 | return p.stdout 34 | 35 | def parse_make_db(s): 36 | # parse the `make -p` output 37 | 38 | db_list = s.split("\n") 39 | 40 | defaults = [] 41 | automatics = [] 42 | 43 | db_list_iter = iter(db_list) 44 | for oneline in db_list_iter: 45 | # capture internal variables 46 | if oneline == "# default": 47 | oneline = next(db_list_iter) 48 | # skip internal variables 49 | if oneline[0] != '.': 50 | defaults.append(oneline) 51 | elif oneline == "# automatic": 52 | oneline = next(db_list_iter) 53 | if oneline[0] != '#': 54 | automatics.append(oneline) 55 | 56 | return defaults, automatics 57 | 58 | def fetch_database(): 59 | # -p won't run a makefile but let's make doubly sure 60 | return parse_make_db(run_make("make -p -f /dev/null")) 61 | 62 | -------------------------------------------------------------------------------- /examples/parse.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | # 4 | # Demo parsing. 5 | # 6 | # run with: 7 | # PYTHONPATH=. python3 examples/parse.py 8 | # 9 | # davep 20241129 10 | 11 | import logging 12 | 13 | import pymake.source as source 14 | import pymake.vline as vline 15 | from pymake.pymake import parse_vline 16 | from pymake.scanner import ScannerIterator 17 | 18 | from gnu_make import run_gnu_make, debug_save 19 | 20 | logger = logging.getLogger("pymake") 21 | 22 | # A list of lines as if read from a makefile. 23 | test_file = """ 24 | # build hello, world 25 | CC?=gcc 26 | CFLAGS?=-Wall 27 | 28 | EXE:=hello 29 | OBJ:=hello.o 30 | 31 | ifdef DEBUG 32 | CFLAGS+=-g 33 | endif 34 | 35 | all: $(EXE) 36 | echo successfully built $(EXE) 37 | 38 | hello : hello.o 39 | $(CC) $(CFLAGS) -o $@ $^ 40 | 41 | hello.o : hello.c 42 | $(CC) $(CFLAGS) -c -o $@ $^ 43 | 44 | hello.c: 45 | echo 'int main(){}' > hello.c 46 | 47 | clean : ; $(RM) $(OBJ) $(EXE) 48 | """ 49 | 50 | def main(): 51 | name = "parser-block-test" 52 | 53 | src = source.SourceString(test_file) 54 | src.load() 55 | 56 | debug_save(src.file_lines) 57 | 58 | # verify everything works in GNU Make 59 | run_gnu_make(src.file_lines) 60 | 61 | # iterator across all actual lines of the makefile 62 | # (supports pushback) 63 | line_scanner = ScannerIterator(src.file_lines, src.name) 64 | 65 | # iterator across "virtual" lines which handles the line continuation 66 | # (backslash) 67 | vline_iter = vline.get_vline(name, line_scanner) 68 | 69 | for virt_line in vline_iter: 70 | s = str(virt_line) 71 | print("%s" % s) 72 | stmt = parse_vline( virt_line, vline_iter ) 73 | assert stmt 74 | print(stmt) 75 | 76 | if __name__ == '__main__': 77 | # logging.basicConfig(level=logging.DEBUG) 78 | logging.basicConfig(level=logging.INFO) 79 | logging.getLogger("pymake.tokenize").setLevel(level=logging.DEBUG) 80 | # logging.getLogger("pymake.parser").setLevel(level=logging.DEBUG) 81 | main() 82 | 83 | -------------------------------------------------------------------------------- /pymake/scanner.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | # 4 | # Scanner (as in scanner for tokenizer/parser) 5 | # Needed an iterator that supports pushback and state save/restore. 6 | # (fancy shmancy https://en.wikipedia.org/wiki/Pushdown_automaton) 7 | 8 | import string 9 | import logging 10 | 11 | logger = logging.getLogger("pymake.scanner") 12 | 13 | class ScannerIterator(object): 14 | # string iterator that allows look ahead and push back 15 | # can also push/pop state (for deep lookaheads) 16 | def __init__(self, data, name): 17 | logger.debug("ScannerIterator datalen=%d", len(data)) 18 | self.data = data 19 | self.filename = name 20 | self.idx = 0 21 | self.max_idx = len(self.data) 22 | self.state_stack = [] 23 | 24 | def __iter__(self): 25 | return self 26 | 27 | def next(self): 28 | return self.__next__() 29 | 30 | def __next__(self): 31 | if self.idx >= self.max_idx: 32 | raise StopIteration 33 | self.idx += 1 34 | return self.data[self.idx-1] 35 | 36 | def lookahead(self): 37 | if self.idx >= self.max_idx: 38 | return None 39 | return self.data[self.idx] 40 | 41 | def pushback(self): 42 | if self.idx <= 0 : 43 | raise StopIteration 44 | self.idx -= 1 45 | 46 | def push_state(self): 47 | self.state_stack.append(self.idx) 48 | 49 | def pop_state(self): 50 | self.idx = self.state_stack.pop() 51 | 52 | def clear_state(self): 53 | # remove previous state pushed but do not restore it 54 | # (we pushed but decided we didn't need to pop) 55 | _ = self.state_stack.pop() 56 | 57 | def remain(self): 58 | # Test/debug method. Return what remains of the data. 59 | return self.data[self.idx:] 60 | 61 | def is_empty(self): 62 | return self.idx >= self.max_idx 63 | 64 | def is_starting(self): 65 | return self.idx == 0 66 | 67 | def get_pos(self): 68 | return self.data[self.idx].get_pos() 69 | 70 | -------------------------------------------------------------------------------- /pymake/hexdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/142812 4 | 5 | import logging 6 | logger = logging.getLogger("hexdump") 7 | 8 | FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) 9 | 10 | def dump(src, length=8): 11 | N=0; result='' 12 | while src: 13 | s,src = src[:length],src[length:] 14 | hexa = ' '.join(["%02X"%ord(x) for x in s]) 15 | s = s.translate(FILTER) 16 | result += "%04X %-*s %s\n" % (N, length*3, hexa, s) 17 | N+=length 18 | return result 19 | 20 | #def dump2(src, length=8): 21 | # result=[] 22 | # for i in xrange(0, len(src), length): 23 | # s = src[i:i+length] 24 | # hexa = ' '.join(["%02X"%ord(x) for x in s]) 25 | # printable = s.translate(FILTER) 26 | # result.append("%04X %-*s %s\n" % (i, length*3, hexa, printable)) 27 | # return ''.join(result) 28 | 29 | def parse_hexdump( lines_list ) : 30 | 31 | # davep 01-Oct-2013 ; moving this function from calpy/pdparse.py into 32 | # hexdump.py. Seems to make more sense here. 33 | """Reverses a hexdump into an array of bytes. Parse a hexdump from an array 34 | of strings. Ignores lines that doesn't start with '0x'.""" 35 | 36 | bytestr = "" 37 | 38 | for offset,line in enumerate(lines_list) : 39 | 40 | line = line.strip() 41 | 42 | # skip junk lines 43 | if not line.startswith( "0x" ) : 44 | logger.warn( "line #{0} skip bad line \"{1}\"".format(offset,line) ) 45 | continue 46 | 47 | fields = line.split( " " ) 48 | # print len(fields), fields 49 | 50 | hex_digits = fields[1].split() 51 | if len(hex_digits) != 16 : 52 | errmsg = "line #{0} invalid hexdump found \"{1}\"".format(offset,line) 53 | raise Exception( errmsg ) 54 | 55 | bytestr += "".join([ chr(int(n,16)) for n in hex_digits ] ) 56 | 57 | return bytestr 58 | 59 | if __name__ == '__main__' : 60 | logging.basicConfig() 61 | 62 | logger.setLevel( level=logging.DEBUG ) 63 | 64 | s=("This 10 line function is just a sample of python power " 65 | "for string manipulations.\n" 66 | "The code is \x07even\x08 quite readable!") 67 | 68 | logger.debug( dump(s, 16) ) 69 | # print( dump2(s, 16 ) ) 70 | 71 | -------------------------------------------------------------------------------- /tests/everything.mk: -------------------------------------------------------------------------------- 1 | # (almost) all Make features in one file 2 | # 3 | # davep 4-Dec-204 4 | 5 | FOO=foo 6 | BAR:=bar 7 | BAZ+=baz 8 | XYZZY?=xyzzy 9 | #NULL ::=null 10 | #BSOD!=bsod 11 | 12 | vpath %.c src 13 | 14 | export RTFM=WTF 15 | export id10t=davep 16 | export 17 | export FOO BAR BAZ XYZZY 18 | 19 | unexport RTFM 20 | unexport BAR BAZ 21 | 22 | # pymake doesn't support yet 23 | #define range 24 | #$(if $(word $(1),$(2)),$(2),$(call range,$(1),$(2) $(words $(2)))) 25 | #endef 26 | 27 | a=$(FOO)$(BAR)$(BAZ) 28 | b=$(FOO) $(BAR) $(BAZ) 29 | deadbeef := dead$(XYZZY)beef 30 | deadbeef := dead$(XYZZY)beef 31 | 32 | # if/else/else/else/endif 33 | count?=3 34 | ifeq ($(count),1) 35 | $(info Thou count to three, no more, no less.) 36 | else ifeq ($(count),2) 37 | $(info Neither count thou two, excepting that thou then proceed to three.) 38 | else ifeq ($(count),3) 39 | $(info Lobbest thou thy Holy Hand Grenade of Antioch towards thy foe,\ 40 | who being naughty in My sight, shall snuff it.) # amen 41 | else ifeq ($(count),4) 42 | $(info Four shalt thou not count.) 43 | else ifeq ($(count),5) 44 | # three, sir! 45 | $(info Five is right out.) 46 | else 47 | $(error blew thyself up) 48 | endif 49 | 50 | all : or nothing 51 | @echo all 52 | 53 | # target specific variable 54 | foo : FOO=more foo! 55 | foo : 56 | @echo $(FOO) 57 | 58 | bar baz xyzzy : foo 59 | @echo $@ 60 | @echo $< 61 | @echo $? 62 | 63 | # pymake can't parse yet 64 | # order only prerequisite 65 | #bsod : windows | msdos 66 | 67 | # pymake can't parse yet 68 | # static pattern rule 69 | #gigo : garbage-in : garbage-out 70 | 71 | sometimes=y 72 | vowels=a e i o u $(sometimes) 73 | alphabet = a b c d e f g h i j k l m n o p q r s t u v w x y z 74 | consonants = $(filter-out $(vowels),$(alphabet)) 75 | $(info $(consonants)) 76 | soup : $(alphabet) 0 1 2 3 4 5 6 7 9 77 | internet_startup : $(consonants) 78 | 79 | # double colon rule 80 | USER=davep 81 | pebkac :: $(USER)chair 82 | pebkac :: problem 83 | pebkac :: $(USER)keyboard 84 | 85 | xon : xoff 86 | xoff: xon 87 | 88 | include smallest.mk 89 | sinclude notexist.mk 90 | -include notexist.mk 91 | 92 | % : ; @echo {implicit} $@ 93 | 94 | -------------------------------------------------------------------------------- /examples/tokeniz.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) David Poole david.poole@ericsson.com 3 | 4 | # Demo simple tokenizing 5 | # 6 | # run with: 7 | # PYTHONPATH=. python3 examples/tokeniz.py 8 | # 9 | # davep 20241124 10 | 11 | import logging 12 | 13 | import pymake.source as source 14 | import pymake.vline as vline 15 | from pymake.scanner import ScannerIterator 16 | from pymake import tokenizer 17 | 18 | from gnu_make import run_gnu_make, debug_save 19 | 20 | logger = logging.getLogger("pymake") 21 | 22 | def main(): 23 | name = "expression-test" 24 | 25 | # A list of lines as if read from a makefile. 26 | # (must be a valid makefile for this test) 27 | test_file = """ 28 | ifeq (a,b) 29 | endif 30 | a:=b 31 | a := b 32 | ifeq 'a' 'b' 33 | endif 34 | 35 | $(info a=$(a)) 36 | a:= 37 | $(a) 38 | 39 | all: $(SRC) 40 | """ 41 | src = source.SourceString(test_file) 42 | src.load() 43 | 44 | debug_save(src.file_lines) 45 | 46 | # run through GNU Make to verify we're valid 47 | run_gnu_make(src.file_lines) 48 | 49 | # iterator across all actual lines of the makefile 50 | # (supports pushback) 51 | line_scanner = ScannerIterator(src.file_lines, src.name) 52 | 53 | # iterator across "virtual" lines which handles the line continuation 54 | # (backslash) 55 | vline_iter = vline.get_vline(name, line_scanner) 56 | 57 | for virt_line in vline_iter: 58 | s = str(virt_line).strip() 59 | print(f"input=\"{s}\"") 60 | 61 | vchar_scanner = iter(virt_line) 62 | 63 | # a very simple tokenize pass that simply splits into Literals (whitespace or non-whitespace) 64 | # and variable ref $() 65 | # Pretty much anything can be tokenized into a simple token_list 66 | token_list = tokenizer.tokenize_line(vchar_scanner) 67 | assert isinstance(token_list,list), type(token_list) 68 | print(token_list) 69 | 70 | if __name__ == '__main__': 71 | # logging.basicConfig(level=logging.DEBUG) 72 | logging.basicConfig(level=logging.INFO) 73 | # logging.getLogger("pymake.tokenize").setLevel(level=logging.DEBUG) 74 | # logging.getLogger("pymake.parser").setLevel(level=logging.DEBUG) 75 | main() 76 | 77 | -------------------------------------------------------------------------------- /pymake/source.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | 4 | import io 5 | 6 | from pymake.error import * 7 | 8 | __all__ = [ "SourceFile" ] 9 | 10 | class Source(object): 11 | def __init__(self, name): 12 | self.name = name 13 | self.file_lines = [] 14 | 15 | def load(self): 16 | # child class must implement 17 | raise NotImplementedError 18 | 19 | # 3.2 What Name to Give Your Makefile 20 | # 21 | # By default, when make looks for the makefile, it tries the following names, 22 | # in order: GNUmakefile, makefile and Makefile. 23 | # 24 | # Normally you should call your makefile either makefile or Makefile. (We 25 | # recommend Makefile because it appears prominently near the beginning of a 26 | # directory listing, right near other important files such as README.) The 27 | # first name checked, GNUmakefile, is not recommended for most makefiles. You 28 | # should use this name if you have a makefile that is specific to GNU make, and 29 | # will not be understood by other versions of make. Other make programs look 30 | # for makefile and Makefile, but not GNUmakefile. 31 | # 32 | class SourceFile(Source): 33 | default_names = ("GNUmakefile", "makefile", "Makefile") 34 | 35 | def __init__(self, name=None): 36 | super().__init__(name) 37 | 38 | def _load_default(self): 39 | for name in self.default_names: 40 | try: 41 | with open(name, 'r') as infile : 42 | self.file_lines = infile.readlines() 43 | self.name = name 44 | return 45 | except FileNotFoundError: 46 | continue 47 | raise NoMakefileFound 48 | 49 | def load(self): 50 | if self.name is None: 51 | return self._load_default() 52 | 53 | with open(self.name, 'r') as infile : 54 | self.file_lines = infile.readlines() 55 | 56 | 57 | class SourceString(Source): 58 | def __init__(self, input_str): 59 | super().__init__("...string") 60 | self.infile = io.StringIO(input_str) 61 | 62 | def load(self): 63 | self.file_lines = self.infile.readlines() 64 | 65 | -------------------------------------------------------------------------------- /pymake/functions_cond.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | # 4 | # functions for conditionals 5 | 6 | from pymake.functions_base import Function, FunctionWithArguments 7 | 8 | __all__ = [ "AndClass", "IfClass", "OrClass" ] 9 | 10 | class AndClass(FunctionWithArguments): 11 | name = "and" 12 | num_args = -1 # no max args 13 | 14 | # "Each argument is expanded, in order. If an argument expands to an empty 15 | # string the processing stops and the result of the expansion is the empty 16 | # string. If all arguments expand to a non-empty string then the result of 17 | # the expansion is the expansion of the last argument." -- gnu_make.pdf 18 | def eval(self, symbol_table): 19 | # breakpoint() 20 | for arg in self.args: 21 | result = "".join([a.eval(symbol_table) for a in arg]) 22 | if not len(result): 23 | return "" 24 | return result 25 | 26 | 27 | class IfClass(FunctionWithArguments): 28 | name = "if" 29 | # required args 2, optional args 3, 30 | # anything > 3 lumped together with the 3rd 31 | # (min,max) 32 | num_args = 3 33 | 34 | def eval(self, symbol_table): 35 | result = "".join([a.eval(symbol_table) for a in self.args[0]]) 36 | if len(result): 37 | return "".join([a.eval(symbol_table) for a in self.args[1]]) 38 | else: 39 | # args[3] should be Literal(",") 40 | # assert len(self.token_list[3].string)==1 and self.token_list[3].string[0].char == ',' 41 | # breakpoint() 42 | return "".join([a.eval(symbol_table) for args in self.args[2:] for a in args]) 43 | 44 | class OrClass(FunctionWithArguments): 45 | name = "or" 46 | num_args = -1 # no max 47 | 48 | # "Each argument is expanded, in order. If an argument expands to a 49 | # non-empty string the processing stops and the result of the expansion is 50 | # that string." -- gnu_make.pdf 51 | def eval(self, symbol_table): 52 | for arg in self.args: 53 | result = "".join([a.eval(symbol_table) for a in arg]) 54 | if len(result): 55 | return result 56 | return "" 57 | 58 | -------------------------------------------------------------------------------- /tests/filter.mk: -------------------------------------------------------------------------------- 1 | # davep 20220902 ; let's figure out how GNU Make's filter() sees the world. 2 | 3 | x:=aa aa aa aa aa aa aa aa aa aa aa bb 4 | 5 | $(info 5 x=$(filter $(strip $(x)) $(x) $(shell seq 1 10)aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 6 | $(info 5 x=$(filter aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 7 | $(info 1 x=$(filter aa,$(x))) 8 | $(info 2 x=$(filter aa,$(x) bb bb bb )) 9 | $(info 3 x=$(filter aa,$(x) bb bb bb aa aa aa)) 10 | $(info 4 x=$(filter aa bb,$(x) bb bb bb aa aa aa)) 11 | $(info 5 x=$(filter aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 12 | $(info 6 x=$(filter $(x) bb bb bb aa aa aa,aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz)) 13 | $(info 7 x=$(filter aa,aa aa aa aa aa aa aa aa aa aa aa bb)) 14 | $(info 7 x=$(filter aa, aa aa aa aa aa aa aa aa aa aa aa bb )) 15 | $(info 7 x=$(filter aa aa, aa aa aa aa aa aa aa aa aa aa aa bb )) 16 | $(info 7 x=$(filter aa,aa aa aa aa aa aa aa aa aa aa aa bb)) 17 | 18 | a:=aa 19 | $(info 8 x=$(filter $(a),$(x))) 20 | b:=bb 21 | $(info 9 x=$(filter $(a) $(b),$(x))) 22 | 23 | $(info a x=$(filter $(a)$(b),$(x)bb $(x)b)) 24 | 25 | $(info b x=$(filter $(a), a)) 26 | $(info c x=$(filter aa,aa,aa,aa,aa,aa)) 27 | 28 | acomma=a,a 29 | $(info acomma x=$(filter $(acomma),aa,aa,aa,aa,aa)) 30 | $(info acomma x=$(filter $(acomma),aa,aa aa,aa aa)) 31 | $(info acomma x=$(filter aa $(acomma),aa,aa aa,aa aa)) 32 | 33 | # confusing but I think Make is seeing this 34 | # as filter "a" in ("a", ",", "a", ",", ... "a") 35 | $(info x=>>$(filter aa, aa , aa , aa , aa , aa)<<) 36 | 37 | comma=, 38 | $(info comma x=$(filter $(comma),aa,aa,aa,aa,aa)) 39 | $(info comma x=$(filter $(comma),aa , aa , aa , aa , aa)) 40 | 41 | x= aa bb cc dd ee ff gg 42 | $(info spaces x=$(filter aa bb,$x)) 43 | $(info spaces x=$(filter aa bb , $x )) 44 | $(info spaces x=$(filter aa bb $(comma) , $x $(comma) $(comma) )) 45 | 46 | # wildcards 47 | SRC=hello.c there.c all.c you.c rabbits.c lol.S foo.h 48 | $(info cfiles=$(filter %.c,$(SRC))) 49 | $(info hc=$(filter h%.c,$(SRC))) 50 | 51 | $(info cS=$(filter %.c %.S,$(SRC))) 52 | 53 | @:;@: 54 | 55 | -------------------------------------------------------------------------------- /tests/filter-out.mk: -------------------------------------------------------------------------------- 1 | x:=aa aa aa aa aa aa aa aa aa aa aa bb 2 | 3 | $(info 5 x=$(filter-out $(strip $(x)) $(x) $(shell seq 1 10)aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 4 | $(info 5 x=$(filter-out aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 5 | $(info 1 x=$(filter-out aa,$(x))) 6 | $(info 2 x=$(filter-out aa,$(x) bb bb bb )) 7 | $(info 3 x=$(filter-out aa,$(x) bb bb bb aa aa aa)) 8 | $(info 4 x=$(filter-out aa bb,$(x) bb bb bb aa aa aa)) 9 | $(info 5 x=$(filter-out aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz,$(x) bb bb bb aa aa aa)) 10 | $(info 6 x=$(filter-out $(x) bb bb bb aa aa aa,aa bb cc dd ee ff gg hh ii jj kk ll mm nn oo pp qq rr ss tt uu vv ww xx yy zz)) 11 | $(info 7 x=$(filter-out aa,aa aa aa aa aa aa aa aa aa aa aa bb)) 12 | $(info 7 x=$(filter-out aa, aa aa aa aa aa aa aa aa aa aa aa bb )) 13 | $(info 7 x=$(filter-out aa aa, aa aa aa aa aa aa aa aa aa aa aa bb )) 14 | $(info 7 x=$(filter-out aa,aa aa aa aa aa aa aa aa aa aa aa bb)) 15 | 16 | a:=aa 17 | $(info 8 x=$(filter-out $(a),$(x))) 18 | b:=bb 19 | $(info 9 x=$(filter-out $(a) $(b),$(x))) 20 | 21 | $(info a x=$(filter-out $(a)$(b),$(x)bb $(x)b)) 22 | 23 | $(info b x=$(filter-out $(a), a)) 24 | $(info c x=$(filter-out aa,aa,aa,aa,aa,aa)) 25 | 26 | acomma=a,a 27 | $(info acomma x=$(filter-out $(acomma),aa,aa,aa,aa,aa)) 28 | $(info acomma x=$(filter-out $(acomma),aa,aa aa,aa aa)) 29 | $(info acomma x=$(filter-out aa $(acomma),aa,aa aa,aa aa)) 30 | 31 | # confusing but I think Make is seeing this 32 | # as filter-out "a" in ("a", ",", "a", ",", ... "a") 33 | $(info x=>>$(filter-out aa, aa , aa , aa , aa , aa)<<) 34 | 35 | comma=, 36 | $(info comma x=$(filter-out $(comma),aa,aa,aa,aa,aa)) 37 | $(info comma x=$(filter-out $(comma),aa , aa , aa , aa , aa)) 38 | 39 | x= aa bb cc dd ee ff gg 40 | $(info spaces x=$(filter-out aa bb,$x)) 41 | $(info spaces x=$(filter-out aa bb , $x )) 42 | $(info spaces x=$(filter-out aa bb $(comma) , $x $(comma) $(comma) )) 43 | 44 | # wildcards 45 | SRC=hello.c there.c all.c you.c rabbits.c lol.S foo.h 46 | $(info cfiles=$(filter-out %.c,$(SRC))) 47 | $(info hc=$(filter-out h%.c,$(SRC))) 48 | 49 | $(info cS=$(filter-out %.c %.S,$(SRC))) 50 | 51 | @:;@: 52 | 53 | -------------------------------------------------------------------------------- /tests/math.mk: -------------------------------------------------------------------------------- 1 | # How is there a way to do arbitrary arthimatic with 100% pure Make? 2 | # 3 | # davep 04-Nov-2014 4 | 5 | include mkseq.mk 6 | ifndef mkseq 7 | $(error missing mkseq) 8 | endif 9 | 10 | include range.mk 11 | ifndef range 12 | $(error missing range) 13 | endif 14 | 15 | # Commutative compare two strings (numbers) 16 | # $1,$2 operands 17 | # $3,$4 comparators 18 | # if ($1==$3 and $2==$4) or ($1==$4 and $2==$3) then true else false 19 | ccmp = $(or $(and $(findstring $1,$3),$(findstring $2,$4)),\ 20 | $(and $(findstring $1,$4),$(findstring $2,$3))) 21 | 22 | # Calculate x+y by counting the words in string expansions. The mkseq calls 23 | # will create sequences of '1's of length $1 and of length $2 24 | # TODO Only handles natural numbers (n>0) 25 | add=$(words $(call mkseq,$1,1) $(call mkseq,$2,1)) 26 | 27 | # a > b? 28 | # a > b if a-b != 0 29 | greater=$(if $(filter-out $(call range,$2),$(call range,$1)),1,) 30 | 31 | # a < b? 32 | # a < b if b-a != 0 33 | lesser=$(if $(filter-out $(call range,$1),$(call range,$2)),1,) 34 | 35 | # a==b ? 36 | # a==b if !a>b and !a !(a>b or as no problem 34 | define foo 35 | echo foo 36 | endef 37 | 38 | # leading creates confusion 39 | # endef leads to error "missing 'enddef', unterminated 'define' 40 | define foo 41 | echo foo 42 | endef 43 | endef # this closes the define, not the endef on the line before 44 | 45 | define foo 46 | 47 | endef 48 | 49 | # directives can be in RHS of rule 50 | 51 | all : DEFINE ENDEF UNDEFINE IFDEF IFNDEF ELSE ENDIF INCLUDE \ 52 | SINCLUDE OVERRIDE EXPORT UNEXPORT PRIVATE VPATH 53 | @echo rule $@ 54 | 55 | # treated as conditional directive but looks like a rule 56 | ifdef : 57 | @echo I am here $@ 58 | endif 59 | 60 | # Some directives cannot in in LHS of oneline rule. 61 | # Those commented out are not valid. 62 | 63 | #define : ; @echo define $@ 64 | #endef : ; @echo endef $@ 65 | undefine : ; @echo rule $@ 66 | #ifdef : ; @echo ifdef $@ 67 | #ifndef : ; @echo ifndef $@ 68 | #else : ; @echo else $@ 69 | #endif : ; @echo endif $@ 70 | include : ; @echo include $@ 71 | -include : ; @echo -- -include $@ 72 | sinclude : ; @echo sinclude $@ 73 | override : ; @echo override $@ 74 | export : ; @echo export=$@ 75 | unexport : ; @echo rule $@ 76 | private : ; @echo rule $@ 77 | vpath : ; @echo rule $@ 78 | 79 | 80 | # some directives can be used as names 81 | private=42 82 | $(info private 42=$(private)) 83 | export=43 84 | $(info export 43=$(export)) 85 | include=44 86 | $(info include 44=$(include)) 87 | endef=45 88 | $(info endef 45=$(endef)) 89 | undefine=46 90 | $(info undefine 46=$(undefine)) 91 | ifdef=47 92 | $(info ifdef 47=$(ifdef)) 93 | 94 | vpath=47 95 | 96 | # this causes some confusion 97 | define : #; @echo $@ 98 | endef 99 | $(info :=$(:)) 100 | 101 | # multi-line variable 102 | define xyzzy = 103 | @echo foo 104 | @echo bar 105 | endef 106 | 107 | $(info xyzz=$(xyzzy)) 108 | 109 | # qqq => ifdef (I can't believe this works!) 110 | qqq:=ifdef 111 | bar:=foo 112 | $(qqq) bar 113 | # holy cheese, we hit this code 114 | $(info bar=$(bar)) 115 | endif 116 | 117 | % : ; @echo implicit $@ 118 | 119 | -------------------------------------------------------------------------------- /tests/subst.mk: -------------------------------------------------------------------------------- 1 | # davep 20170225 ; subst first real function to be implemented 2 | 3 | $(info $(subst ee,EE,feet on the street)) 4 | $(info $(subst ee,EE,feet on the street <<)) 5 | 6 | $(info b=$(subst a,b,a a a a a a)) # b b b b b b 7 | 8 | feet:=feet 9 | street:=street 10 | $(info $(subst ee,EE,$(feet) on the $(street))) 11 | 12 | # make seems to ignore whitespace between "subst" and first arg 13 | path:=$(subst :, dave ,$(PATH)) 14 | $(info p1 $(path)) 15 | 16 | path:=$(subst :, ,$(PATH)) 17 | $(info p2 $(path)) 18 | 19 | q=q 20 | path:=$(subst :, $q ,$(PATH)) 21 | $(info p3 $(path)) 22 | 23 | space= 24 | path:=$(subst :,$(space),$(PATH)) 25 | $(info p4 $(path)) 26 | 27 | 28 | path:=$(PATH) 29 | split_path=$(subst :, ,$(path)) 30 | path=foo:bar:baz: 31 | $(info p5 $(split_path)) 32 | 33 | # need a literal comma must use a intermediate var 34 | comma=, 35 | s:=The,Quick,Brown,Fox,Jumped,Over,The,Lazy,Dogs 36 | $(info $(subst $(comma), ,$s)) 37 | 38 | $(info $(subst $(comma),$q,$s)) 39 | 40 | alphabet=abcdefghijklmnopqrstuvwxyz 41 | $(info alphabet=$(alphabet)) 42 | # for LOLs 43 | UPPERCASE=$(subst z,Z,$(subst y,Y,$(subst x,X,$(subst w,W,$(subst v,V,$(subst u,U,$(subst t,T,$(subst s,S,$(subst r,R,$(subst q,Q,$(subst p,P,$(subst o,O,$(subst n,N,$(subst m,M,$(subst l,L,$(subst k,K,$(subst j,J,$(subst i,I,$(subst h,H,$(subst g,G,$(subst f,F,$(subst e,E,$(subst d,D,$(subst c,C,$(subst b,B,$(subst a,A,$(target))))))))))))))))))))))))))) 44 | 45 | target:=$(alphabet) 46 | $(info ALPHABET=$(UPPERCASE)) 47 | 48 | target:=The quick brown fox jumped over the lazy dogs 49 | $(info UPPERCASE=$(UPPERCASE)) 50 | 51 | # error cases <=2 commas 52 | #$(info >$(subst )<) # *** insufficient number of arguments (1) to function `subst'. Stop. 53 | #$(info >$(subst ,)<) # *** insufficient number of arguments (2) to function `subst'. Stop. 54 | 55 | # FIXME this fails because my parse_args doesn't handle it 56 | #$(info empty=>$(subst ,,)<) # empty 57 | 58 | $(info 1 $(subst foo,bar,foobarbaz)) 59 | $(info 2 $(subst foo,bar,foo bar baz)) 60 | $(info 3 $(subst foo,bar,foo bar baz)) # WS preserved! 61 | $(info 3 >>$(subst foo,bar, foo bar baz )<<) # WS preserved! 62 | $(info 3 >>$(strip $(subst foo,bar, foo bar baz ))<<) # WS lost by strip() 63 | 3:=>>$(strip $(subst foo,bar, foo bar baz ))<< # WS lost by strip() 64 | $(info 3=$3) 65 | 66 | # pattern strings with embedded spaces 67 | $(info 1 spaces=$(subst foo bar baz,qqq,foo bar baz)) # qqq 68 | foobarbaz=foo bar baz 69 | $(info 2 spaces=$(subst foo bar baz,qqq,foo bar baz $(foobarbaz))) # qqq qqq 70 | 71 | $(info empty=$(subst,z,a b c d e f g)) # empty string!? (weird) 72 | $(info empty=$(subst ,z,a b c d e f g)) # a b c d e f gz (wtf?) 73 | $(info empty=$(subst ,z,a b c d e f g)) # a b c d e f gz 74 | $(info empty=$(subst ,z, a b c d e f g )) # a b c d e f g z 75 | $(info empty=$(subst a,,a b c d e f g)) # b c d e f g (remove a) 76 | $(info empty=$(subst ,,a b c d e f g)) # a b c d e f g (no change) 77 | 78 | @:;@: 79 | 80 | -------------------------------------------------------------------------------- /tests/test_env_recurse.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | 4 | # Test recursively expanded variables and their interaction with the shell 5 | 6 | import pytest 7 | 8 | import run 9 | import verify 10 | 11 | def test1(): 12 | makefile=""" 13 | FOO=$(shell echo foo) 14 | export FOO 15 | 16 | all: 17 | @printenv FOO 18 | """ 19 | expect = ( 20 | "foo", 21 | ) 22 | 23 | p = run.gnumake_string(makefile) 24 | verify.compare_result_stdout(expect, p) 25 | 26 | p = run.pymake_string(makefile) 27 | verify.compare_result_stdout(expect, p) 28 | 29 | def test_not_exported(): 30 | makefile=""" 31 | FOO=$(shell echo foo) 32 | 33 | all: 34 | @printenv FOO 35 | """ 36 | p = run.gnumake_should_fail(makefile) 37 | 38 | p = run.pymake_should_fail(makefile) 39 | 40 | @pytest.mark.skip("TODO GNU Make 4.3 has different behaviors") 41 | def test_export_loop(): 42 | makefile=""" 43 | FOO=$(shell printenv BAR && echo BAR ok) 44 | BAR=$(shell printenv FOO && echo FOO ok) 45 | 46 | export FOO BAR 47 | 48 | all: 49 | @printenv FOO 50 | @printenv BAR 51 | """ 52 | expect = ( 53 | "FOO ok BAR ok", 54 | "BAR ok FOO ok" 55 | ) 56 | 57 | p = run.gnumake_string(makefile) 58 | verify.compare_result_stdout(expect, p) 59 | 60 | p = run.pymake_string(makefile) 61 | verify.compare_result_stdout(expect, p) 62 | 63 | @pytest.mark.skip("TODO GNU Make 4.3 has different behaviors") 64 | def test_self_export(): 65 | # FOO must be in the environment but empty. 66 | makefile=""" 67 | FOO=$(shell printenv FOO && echo FOO ok) 68 | 69 | export FOO 70 | 71 | all: 72 | @printenv FOO 73 | """ 74 | expect = ( 75 | "FOO ok", 76 | ) 77 | 78 | p = run.gnumake_string(makefile) 79 | verify.compare_result_stdout(expect, p) 80 | 81 | p = run.pymake_string(makefile) 82 | verify.compare_result_stdout(expect, p) 83 | 84 | def test_unaccessed_self_reference(): 85 | # a definite self reference but FOO never accessed so who cares 86 | makefile=""" 87 | FOO=$(FOO) 88 | 89 | @:;@: 90 | """ 91 | run.simple_test(makefile) 92 | 93 | def test_self_reference(): 94 | # a definite self reference 95 | makefile=""" 96 | FOO=$(FOO) 97 | $(info FOO=$(FOO)) 98 | @:;@: 99 | """ 100 | 101 | expect = "*** Recursive variable 'FOO' references itself (eventually)." 102 | 103 | s = run.gnumake_should_fail(makefile) 104 | assert expect in s, s 105 | 106 | s = run.pymake_should_fail(makefile) 107 | s_list = s.split("\n") 108 | assert expect in s_list[-1] 109 | 110 | def test_self_reference_rule(): 111 | makefile=""" 112 | export FOO=$(FOO) 113 | 114 | all: ; @printenv FOO 115 | """ 116 | expect = "Recursive variable 'FOO' references itself (eventually)." 117 | 118 | s = run.gnumake_should_fail(makefile) 119 | assert expect in s, s 120 | 121 | s = run.pymake_should_fail(makefile) 122 | s_list = s.split("\n") 123 | assert expect in s_list[-1] 124 | 125 | -------------------------------------------------------------------------------- /pymake/constants.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | 4 | # can't use string.whitespace because want to preserve line endings 5 | whitespace = set(' \t') 6 | 7 | # davep 04-Dec-2014 ; FIXME ::= != are not in Make 3.81, 3.82 (Introduced in 4.0) 8 | # :::= is apparently a POSIX thing (see do_variable_definition()-src/variable.c) 9 | assignment_operators = {"=", "?=", ":=", "::=", "+=", "!=", ":::=" } 10 | rule_operators = {":", "::", "?:" } 11 | eol = set("\r\n") 12 | 13 | # eventually will need to port this thing to Windows' CR+LF 14 | platform_eol = "\n" 15 | 16 | # TODO can be changed by .RECIPEPREFIX 17 | recipe_prefix = "\t" 18 | 19 | backslash = '\\' 20 | 21 | # 4.8 Special Built-In Target Names 22 | built_in_targets = { 23 | ".PHONY", 24 | ".SUFFIXES", 25 | ".DEFAULT", 26 | ".PRECIOUS", 27 | ".INTERMEDIATE", 28 | ".SECONDARY", 29 | ".SECONDEXPANSION", 30 | ".DELETE_ON_ERROR", 31 | ".IGNORE", 32 | ".LOW_RESOLUTION_TIME", 33 | ".SILENT", 34 | ".EXPORT_ALL_VARIABLES", 35 | ".NOTPARALLEL", 36 | ".ONESHELL", 37 | ".POSIX", 38 | } 39 | 40 | # 41 | # Stuff from Appendix A. 42 | # 43 | 44 | 45 | # Conditionals separate because conditionals can be multi-line and require some 46 | # complex handling. 47 | conditional_open = { 48 | "ifdef", "ifndef", 49 | # newer versions of Make? (TODO verify when these appeared) 50 | "ifeq", "ifneq" 51 | } 52 | 53 | conditional_close = { 54 | "else", "endif", 55 | } 56 | 57 | conditional_directive = conditional_open | conditional_close 58 | 59 | assignment_modifier = { 60 | "export", "unexport", 61 | "override", "private", "define", "undefine" 62 | } 63 | 64 | include_directive = { 65 | "include", "-include", "sinclude", 66 | } 67 | 68 | # all directives (pseudo "reserved words") 69 | directive = { 70 | "endef", 71 | "vpath", 72 | } | conditional_directive | assignment_modifier | include_directive 73 | 74 | automatic_variables = { 75 | "@", 76 | "%", 77 | "<", 78 | "?", 79 | "^", 80 | "+", 81 | "*", 82 | "@D", 83 | "@F", 84 | "*D", 85 | "*F", 86 | "%D", 87 | "%F", 88 | " ("this is a test","=","foo") 19 | # this is a test : foo --> ("this","is","a","test",":","foo") 20 | # 21 | # Spaces must be preserved until the rule vs assignment has been 22 | # disambiguated. 23 | # 24 | # 25 | # 26 | # davep 15-sep-2014 27 | 28 | # this is a comment 29 | $(CC)=gcc 30 | 31 | all:a\=b ; @echo all 32 | 33 | backslash=\ 34 | 35 | \?==foo 36 | $(info = foo=$($(backslash)?)) 37 | 38 | \+=+ 39 | $(info @ +@$(+)) 40 | 41 | # error "empty variable name" 42 | #+=+ 43 | #$(info @ +@$(+)) 44 | 45 | # error "empty variable name" 46 | #=== 47 | 48 | # empy RHS -- ok 49 | empty1= 50 | empty2?= 51 | empty3:= 52 | empty4::= 53 | empty5+= 54 | empty6!= 55 | 56 | equal== 57 | $(info @ =@$(equal)) 58 | 59 | # variable assignment masquerading as variable LHS 60 | !==qq 61 | $(info @ !@$(!$(equal))) 62 | 63 | a$(equal)b : c ; @echo a=b 64 | 65 | $(info @ a=b@a=b) 66 | 67 | # raw equal doesn't work as a prerequisite 68 | # error "empty variable name" 69 | #= : ; @echo $@ 70 | $(equal) : ; @echo @=@$@ 71 | 72 | # target "empty7" with prerequisite of "=" 73 | empty7 : = ; @echo $@ 74 | 75 | # This parses. But what does it do? 76 | empty7 :\ = ; @echo empty8 77 | $(info = @echo empty8=$(empty8 : )) 78 | 79 | empty7 := ; @echo empty7 80 | $(info = ; @echo empty7=$(empty7)) 81 | 82 | c:;@echo c 83 | 84 | # aw crap 85 | this is a test = foobarbaz 86 | this is a test : foobarbaz 87 | 88 | lots of leading spaces = aw yis leading spaces 89 | $(info = aw yis leading spaces=$(lots of leading spaces)) 90 | lots of trailing spaces = hell yeah trailing spaces 91 | $(info = hell yeah trailing spaces=$(lots of trailing spaces)) 92 | ok more trailing spaces on rhs = there are six trailing spaces after this last . 93 | $(info = there are six trailing spaces after this last . =$(ok more trailing spaces on rhs)) 94 | leading spaces are ignored I'm sorry to say = all these leading spaces are ignored 95 | $(info = all these leading spaces are ignored=$(leading spaces are ignored I'm sorry to say)) 96 | 97 | # this parses 98 | ? = question 99 | $(info = question=$(?)) 100 | + = plus 101 | $(info = plus=$(+)) 102 | * = star 103 | $(info = star=$(*)) 104 | ~`@$$*!&%\:::=a mess 105 | $(info = a mess=$(~`@$$*!&%\:)) 106 | 107 | $(this is a test) : ; @echo = foobarbaz=$(this is a test) 108 | 109 | hello\ $(this is a test) = $(hello $(this is a test)) 110 | hello\ $(this is a test) : $(hello $(this is a test)) ; @echo = hello foobarbaz=hello foobarbaz 111 | 112 | %:;@echo {implicit rule} $@ 113 | 114 | -------------------------------------------------------------------------------- /examples/rules.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | # 4 | # Demo parsing a rule 5 | # 6 | # run with: 7 | # PYTHONPATH=. python3 examples/rules.py 8 | # 9 | # davep 20241129 10 | 11 | import logging 12 | 13 | import pymake.source as source 14 | import pymake.vline as vline 15 | import pymake.tokenizer as tokenizer 16 | from pymake.scanner import ScannerIterator 17 | 18 | from gnu_make import run_gnu_make, debug_save 19 | 20 | logger = logging.getLogger("pymake") 21 | 22 | test_file=""" 23 | one: 24 | 25 | two : $(TWO) 26 | 27 | three : ; echo foo 28 | 29 | four : FOO:=BAR 30 | four: 31 | 32 | five: FOO?=BAR 33 | five : /dev/null /dev/zero 34 | 35 | # double colon rule 36 | six:: 37 | 38 | seven :: 39 | 40 | $(eight):$(nine)$(ten)$(eleven) 41 | 42 | # static pattern rule (straight from GNU Make manual) 43 | # Not Implemented Yet 44 | # $(objects): %.o: %.c 45 | 46 | # Can have a var assignment with what looks like a a recipe statement following. 47 | # Except if there is an assignment statement, the ; is not a recipe but is 48 | # appended back to the assignment. The following launches a shell 49 | # executing 'ls ; echo' 50 | twelve : FOO!=ls ; echo $(FOO) 51 | twelve : ; @echo found $(words $(FOO)) files 52 | 53 | # NotImplementedError 54 | #thirteen&: 55 | 56 | thirteen& : foo 57 | 58 | fourteen|: 59 | 60 | fifteen? : bar? 61 | 62 | $$sixteen$$ : $$bar$$ 63 | 64 | """ 65 | 66 | error_cases = [ 67 | """ 68 | # Not a valid assignment statement. 69 | # So a dependency on '=' ? 70 | # NOPE! "empty variable name" The '=' is still treated as assignment. 71 | thirteen :: = 72 | """, 73 | 74 | ] 75 | 76 | def main(): 77 | name = "rule-recipe-block-test" 78 | 79 | src = source.SourceString(test_file) 80 | src.load() 81 | 82 | debug_save(src.file_lines) 83 | 84 | # verify everything works in GNU Make 85 | run_gnu_make(src.file_lines) 86 | 87 | # iterator across all actual lines of the makefile 88 | # (supports pushback) 89 | line_scanner = ScannerIterator(src.file_lines, src.name) 90 | 91 | # iterator across "virtual" lines which handles the line continuation 92 | # (backslash) 93 | vline_iter = vline.get_vline(name, line_scanner) 94 | 95 | for virt_line in vline_iter: 96 | vchar_scanner = iter(virt_line) 97 | lhs = tokenizer.tokenize_rule(vchar_scanner) 98 | assert lhs is not None 99 | token_list, rule_op = lhs 100 | print(token_list, rule_op) 101 | print("".join([str(t) for t in token_list])) 102 | 103 | rhs = tokenizer.tokenize_rule_RHS(vchar_scanner) 104 | print("rhs=",rhs) 105 | 106 | # anything left must be pointing to the start of a recipe 107 | if vchar_scanner.remain(): 108 | vchar = vchar_scanner.lookahead() 109 | assert vchar.char == ';', vchar.char 110 | recipe = tokenizer.tokenize_recipe(vchar_scanner) 111 | assert recipe, recipe 112 | print("recipe=",recipe) 113 | 114 | if __name__ == '__main__': 115 | logging.basicConfig(level=logging.INFO) 116 | # logging.getLogger("pymake.tokenize").setLevel(level=logging.DEBUG) 117 | main() 118 | 119 | -------------------------------------------------------------------------------- /examples/conditional.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2024 David Poole david.poole@ericsson.com 3 | 4 | # Demo finding conditional blocks. 5 | # 6 | # run with: 7 | # PYTHONPATH=. python3 examples/conditional.py 8 | # 9 | # davep 20241116 10 | 11 | import logging 12 | 13 | import pymake.source as source 14 | import pymake.vline as vline 15 | from pymake.scanner import ScannerIterator 16 | from pymake import tokenizer 17 | from pymake.constants import * 18 | from pymake.tokenizer import seek_directive 19 | from pymake.parser import parse_directive 20 | 21 | from gnu_make import run_gnu_make, debug_save 22 | 23 | logger = logging.getLogger("pymake") 24 | 25 | def main(): 26 | name = "conditional-block-test" 27 | 28 | # A list of lines as if read from a makefile. 29 | test_file = """ 30 | ifdef SRC 31 | foo 32 | endif 33 | 34 | ifdef SRC 35 | foo 36 | else ifdef OBJ 37 | bar 38 | endif 39 | 40 | ifdef SRC 41 | foo 42 | else 43 | ifdef OBJ 44 | bar 45 | endif 46 | endif 47 | 48 | ifeq (a,b) 49 | endif 50 | 51 | ifneq 'a' 'b' 52 | endif 53 | 54 | # conditional with invalid block 55 | ifdef SRC 56 | this line cannot be parsed by make 57 | endif 58 | 59 | # nested conditional with an invalid conditional within 60 | ifdef SRC 61 | ifeq xyz # invalid but should not fail 62 | endif 63 | endif 64 | 65 | ifeq 'abc' "xyz" 66 | hello, world this is an error in your makefile 67 | endif 68 | """ 69 | src = source.SourceString(test_file) 70 | src.load() 71 | 72 | debug_save(src.file_lines) 73 | 74 | # verify everything works in GNU Make 75 | run_gnu_make(src.file_lines) 76 | 77 | # iterator across all actual lines of the makefile 78 | # (supports pushback) 79 | line_scanner = ScannerIterator(src.file_lines, src.name) 80 | 81 | # iterator across "virtual" lines which handles the line continuation 82 | # (backslash) 83 | vline_iter = vline.get_vline(name, line_scanner) 84 | 85 | for virt_line in vline_iter: 86 | s = str(virt_line).strip() 87 | print(f"input=>>>{s}<<<") 88 | 89 | vchar_scanner = iter(virt_line) 90 | # ha ha type checking 91 | _ = vchar_scanner.pushback 92 | _ = vchar_scanner.get_pos 93 | 94 | # 95 | # closely follow GNU Make's behavior eval() src/read.c 96 | # 97 | # 1. assignments 98 | # 2. conditional blocks 99 | # 3... coming soon 100 | # 101 | expr = tokenizer.tokenize_assignment_statement(vchar_scanner) 102 | if expr: 103 | # Is an assignment statement not a conditional. 104 | # So ignore. 105 | continue 106 | 107 | # make sure tokenize_assignment_statement() restored vchar_scanner to 108 | # its starting state 109 | # 110 | # pos[0] = filename 111 | # pos[1] = (row,col) 112 | pos = vchar_scanner.get_pos() 113 | assert vchar_scanner.get_pos()[1][1] == 0, pos 114 | 115 | # mimic what GNU Make conditional_line() does 116 | # by looking for a directive in this line 117 | vstr = seek_directive(vchar_scanner, conditional_open) 118 | 119 | # for this test, we should always find a directive 120 | assert vstr, vstr 121 | 122 | d = parse_directive( vstr, vchar_scanner, vline_iter) 123 | print(d) 124 | print(f'makefile="""\n{d.makefile()}\n"""') 125 | 126 | 127 | 128 | if __name__ == '__main__': 129 | # logging.basicConfig(level=logging.DEBUG) 130 | logging.basicConfig(level=logging.INFO) 131 | # logging.getLogger("pymake.tokenize").setLevel(level=logging.DEBUG) 132 | # logging.getLogger("pymake.parser").setLevel(level=logging.DEBUG) 133 | main() 134 | 135 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36,37,38,39,310,311,312}-{linux,macos} 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.6: py36 8 | 3.7: py37 9 | 3.8: py38 10 | 3.9: py39 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | pypy3: pypy3 15 | 16 | [gh-actions:env] 17 | PLATFORM = 18 | ubuntu-latest: linux 19 | macos-latest: macos 20 | windows-latest: windows 21 | 22 | 23 | # Testing 24 | # ============================================================================= 25 | 26 | [testenv] 27 | description = Run tests with coverage with pytest under current Python env 28 | usedevelop = true 29 | setenv = COVERAGE_FILE=.coverage_{envname} 30 | passenv = CI 31 | deps = 32 | -rtests/requirements.txt 33 | coverage 34 | commands = 35 | coverage run --source=pymake --parallel-mode -m pytest --doctest-modules --durations=50 --durations-min 1 -vv --timeout=20 {posargs} 36 | coverage combine 37 | coverage report -m 38 | coverage xml 39 | 40 | [testenv:final-coverage] 41 | description = Combine coverage data across environments (run after tests) 42 | skip_install = True 43 | setenv = COVERAGE_FILE=.coverage 44 | passenv = {[testenv]passenv} 45 | deps = coverage 46 | commands = 47 | coverage combine 48 | coverage report -m 49 | coverage xml 50 | 51 | [testenv:codecov] 52 | description = Upload coverage data to codecov (only run on CI) 53 | setenv = 54 | {[testenv:final-coverage]setenv} 55 | passenv = {[testenv]passenv} 56 | deps = codecov 57 | commands = codecov --required 58 | 59 | # ----------------------------------------------------------------------------- 60 | # Linting 61 | # ============================================================================= 62 | 63 | [testenv:pylint] # Will use the configuration file `.pylintrc` automatically 64 | description = Perform static analysis and output code metrics 65 | basepython = python3 66 | skip_install = false 67 | deps = 68 | pylint == 2.5.* 69 | commands = 70 | pylint pymake 71 | 72 | [testenv:docs] 73 | description = Invoke sphinx to build documentation and API reference 74 | basepython = python3 75 | deps = 76 | -rdocs/requirements.txt 77 | commands = 78 | sphinx-build -b html -d build/doctrees -nWT docs/ docs/build/html 79 | 80 | [testenv:checks] 81 | description = Verify code style with pre-commit hooks. 82 | basepython = python3 83 | skip_install = true 84 | deps = 85 | pre-commit 86 | commands = 87 | pre-commit run --all-files 88 | 89 | # ----------------------------------------------------------------------------- 90 | 91 | 92 | 93 | # ----------------------------------------------------------------------------- 94 | # Deployment 95 | # ============================================================================= 96 | 97 | [testenv:packaging] 98 | description = Check whether README.rst is reST and missing from MANIFEST.in 99 | basepython = python3 100 | deps = 101 | check-manifest 102 | readme_renderer 103 | commands = 104 | check-manifest 105 | python setup.py check -r -s 106 | 107 | [testenv:build] 108 | basepython = python3 109 | skip_install = true 110 | deps = 111 | wheel 112 | setuptools 113 | commands = 114 | python setup.py -q sdist bdist_wheel 115 | 116 | 117 | # Tool Configuration 118 | # ============================================================================= 119 | 120 | # Pytest configuration 121 | [pytest] 122 | addopts = -ra -q --color=yes 123 | norecursedirs = .* *.egg* config docs dist build 124 | xfail_strict = True 125 | 126 | # Coverage configuration 127 | [coverage:run] 128 | branch = True 129 | source = 130 | pymake 131 | tests 132 | omit = **/_[a-zA-Z0-9]*.py 133 | 134 | # ----------------------------------------------------------------------------- 135 | -------------------------------------------------------------------------------- /tests/test_args.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | # test pargs.py to make codecov happy 5 | 6 | import pytest 7 | 8 | from pymake import pargs 9 | 10 | def test_usage(): 11 | pargs.usage() 12 | 13 | def test_args(): 14 | a = pargs.Args() 15 | 16 | @pytest.mark.skip(reason="sys.exit causes pytest to error") 17 | def test_parse_args_help(): 18 | args = pargs.parse_args(('-h',)) 19 | args = pargs.parse_args(('--help',)) 20 | 21 | def test_parse_args_always_make(): 22 | args = pargs.parse_args(('-B', '-f', '/dev/null')) 23 | assert args.always_make 24 | 25 | args = pargs.parse_args(('--always-make', '-f', '/dev/null')) 26 | assert args.always_make 27 | 28 | def test_parse_args_file(): 29 | args = pargs.parse_args(('-f', '/dev/null')) 30 | assert args.filename == "/dev/null" 31 | 32 | args = pargs.parse_args(('--file', '/dev/null')) 33 | assert args.filename == "/dev/null" 34 | 35 | args = pargs.parse_args(('--makefile', '/dev/null')) 36 | assert args.filename == "/dev/null" 37 | 38 | def test_parse_args_sexpr(): 39 | args = pargs.parse_args(('-S', '-f', '/dev/null')) 40 | assert args.filename == "/dev/null" 41 | assert args.s_expr 42 | 43 | def test_no_builtin_rules(): 44 | args = pargs.parse_args(('-r', '-f', '/dev/null')) 45 | assert args.filename == "/dev/null" 46 | assert args.no_builtin_rules 47 | 48 | args = pargs.parse_args(('--no-builtin-rules', '-f', '/dev/null')) 49 | assert args.filename == "/dev/null" 50 | assert args.no_builtin_rules 51 | 52 | def test_warn_undefined(): 53 | args = pargs.parse_args(('--warn-undefined-variables', '-f', '/dev/null')) 54 | assert args.filename == "/dev/null" 55 | assert args.warn_undefined_variables 56 | 57 | def test_dotfile(): 58 | args = pargs.parse_args(('--dotfile', 'makefile.dot', '-f', '/dev/null')) 59 | assert args.filename == "/dev/null" 60 | assert args.dotfile == 'makefile.dot' 61 | 62 | def test_html(): 63 | args = pargs.parse_args(('--html', 'makefile.html', '-f', '/dev/null')) 64 | assert args.filename == "/dev/null" 65 | assert args.htmlfile == 'makefile.html' 66 | 67 | def test_directory(): 68 | args = pargs.parse_args(('-C', 'build', '-f', '/dev/null')) 69 | assert args.filename == "/dev/null" 70 | assert len(args.directory)==1 71 | assert args.directory[0] == 'build' 72 | 73 | args = pargs.parse_args(('--directory', 'build', '-f', '/dev/null')) 74 | assert args.filename == "/dev/null" 75 | assert len(args.directory)==1 76 | assert args.directory[0] == 'build' 77 | 78 | def test_multiple_directory(): 79 | args = pargs.parse_args(('-C', 'build', '-C', 'docs' )) 80 | assert len(args.directory)==2 81 | assert args.directory[0] == 'build' 82 | assert args.directory[1] == 'docs' 83 | 84 | args = pargs.parse_args(('--directory', 'build', '--directory', 'docs' )) 85 | assert len(args.directory)==2 86 | assert args.directory[0] == 'build' 87 | assert args.directory[1] == 'docs' 88 | 89 | def test_dry_run(): 90 | args = pargs.parse_args(('-n',),) 91 | assert args.dry_run 92 | 93 | args = pargs.parse_args(('--just-print',),) 94 | assert args.dry_run 95 | 96 | args = pargs.parse_args(('--dry-run',),) 97 | assert args.dry_run 98 | 99 | args = pargs.parse_args(('--recon',),) 100 | assert args.dry_run 101 | 102 | def test_debug(): 103 | args = pargs.parse_args(('-d',),) 104 | assert args.debug 105 | 106 | def test_debug_flag(): 107 | args = pargs.parse_args(('--debug=tokenize',),) 108 | assert args.debug_flags[0] == 'tokenize' 109 | 110 | def test_debug_multiple_flags(): 111 | args = pargs.parse_args(('--debug=tokenize,scanner',),) 112 | assert args.debug_flags[0] == 'tokenize' 113 | assert args.debug_flags[1] == 'scanner' 114 | 115 | def test_debug_flags_foo(): 116 | with pytest.raises(ValueError): 117 | args = pargs.parse_args(('--debug=foo',),) 118 | 119 | -------------------------------------------------------------------------------- /pymake/wildcard.py: -------------------------------------------------------------------------------- 1 | 2 | def split_percent(s): 3 | assert isinstance(s,str), type(s) 4 | 5 | # break a string into sub-string before/after a '%' 6 | # Make sure to check for 7 | # \% (escaped %) 8 | # \\% (escaped backslash which is a literal %) 9 | idx = -1 10 | while 1: 11 | try: 12 | idx = s.index('%', idx+1) 13 | except ValueError: 14 | return None 15 | 16 | # First check for single \ 17 | # Step1: if we have a char before the % and that char is a \ 18 | # 19 | # Next check for double \\ (escaped backslash (literal \) so the % is 20 | # valid) 21 | # Step2: if NOT (we have a char before the \ and that char is a \) 22 | if idx > 0 and s[idx-1] == '\\' and \ 23 | not (idx > 1 and s[idx-2] == '\\'): 24 | # at this point we have the case of a backslash'd % so 25 | # ignore it, go looking for next possible % 26 | continue 27 | 28 | # +1 to skip the % 29 | return s[:idx], s[idx+1:] 30 | 31 | def wildcard_match_list(pattern_list, target_list, negate=False): 32 | # first arg must be a list, not an iterable, because we use it twice 33 | assert isinstance(pattern_list,list), type(pattern_list) 34 | # print(f"pattern_list={pattern_list} target_list={target_list} negate={negate}") 35 | 36 | # pre-calculate all the patterns 37 | p_list = [split_percent(p) for p in pattern_list] 38 | 39 | for t in target_list: 40 | for p,pattern in zip(p_list,pattern_list): 41 | # print(t,p,pattern) 42 | if p is None: 43 | # no '%' so just a string compare 44 | flag = t == pattern 45 | else: 46 | flag = t.startswith(p[0]) and t.endswith(p[1]) 47 | 48 | # print(flag,negate,flag^negate) 49 | # flag==True => match 50 | # flag==False => no-match 51 | # negate==False => filter 52 | # negate==True => filter-out 53 | # 54 | # flag negate desired result 55 | # +------------------------------ 56 | # | true true false (match + filter-out) 57 | # | true false true (no match + filter-out) 58 | # | false true true (match + filter) 59 | # | false false false (no match + filter) 60 | 61 | if flag: 62 | # quit searching on first match (only one match per target) 63 | if not negate: 64 | # if we're filter then we return this target 65 | # if we're filter-out then we skip this target 66 | yield t 67 | break 68 | 69 | else: 70 | # at this point, none of the patterns matched 71 | if negate: 72 | yield t 73 | 74 | def wildcard_match(pattern, strlist): 75 | # backwards compatible layer for some older test code 76 | return list(wildcard_match_list( [pattern], strlist)) 77 | 78 | def wildcard_replace(search, replace, strlist): 79 | # 80 | # Must carefully preserve whitespace!! 81 | # 82 | 83 | s = split_percent(search) 84 | 85 | if s is None: 86 | # no wildcards so just a simple string replace 87 | assert 0 # do I still hit this case? $(patsubst) decaying to $(subst) handled elsewhere 88 | return [replace if search==str_ else str_ for str_ in strlist] 89 | 90 | r = split_percent(replace) 91 | 92 | new_list = [] 93 | for str_ in strlist: 94 | if str_.startswith(s[0]) and str_.endswith(s[1]): 95 | if r is None: 96 | new_list.append(replace) 97 | else: 98 | mid = str_[len(s[0]) : len(str_)-len(s[1])] 99 | new = r[0] + mid + r[1] 100 | new_list.append(new) 101 | else: 102 | new_list.append(str_) 103 | 104 | return new_list 105 | 106 | 107 | -------------------------------------------------------------------------------- /tests/recipe.mk: -------------------------------------------------------------------------------- 1 | # Tinker with recipes. 2 | # davep 23-Sep-2014 3 | 4 | # 5 | # Oh ho. "missing separator" means GNU Make treating :,:: and assignment tokens as separator! 6 | # 7 | 8 | # where do recipes end? 9 | all : 10 | # this is a comment 11 | @echo = all=$@ 12 | # blank line after this comment 13 | 14 | @echo = $@ again=$@ again 15 | # recipe continues 16 | # this is passed to the shell 17 | 18 | # recipe ends where? 19 | echo : echo # looks like rule, valid shell 20 | 21 | 22 | foo=bar # recipe definitely ends here 23 | # empty line with tab is ignored 24 | 25 | # tab then something throws an error "commands commence before first target." 26 | # echo foo 27 | 28 | backslashes : ; @echo back\ 29 | slash 30 | 31 | # tab before "slash2" 32 | backslashes2 : ; @echo back\ 33 | slash2 34 | 35 | # slaces before slash3b 36 | backslashes3 : ; @echo back\ 37 | slash3 38 | @echo back\ 39 | slash3b 40 | # lots of leading spaces collapsed to one space 41 | @echo back\ 42 | slash3c 43 | 44 | where-do-i-end : ; @echo $@ 45 | @echo leading tab 46 | # leading spaces error "missing separator" 47 | # @echo bar 48 | @echo I am still in $@ 49 | 50 | # comment with leading spaces; part of recipe 51 | @echo yet again I am still in $@ 52 | foofoofoo=999 # recipe is done here; leading spaces are ignored (eaten by recipe tokenizer?) 53 | $(info = 999=$(foofoofoo)) # leading spaces on name are ignored 54 | 55 | 56 | whitespace-then-tab : ; @echo $@ 57 | 58 | # leading whitespace then tab is not valid error "missing separator" 59 | # @echo lots of leading whitespace here 60 | 61 | 62 | varrefs : ; @echo $@ 63 | # the $(info) is error "commands commence before first target" 64 | #$(info inside varref recipes) 65 | @echo varref recipe 66 | # this $(info) is not executed (not seeing the message) 67 | @echo varref recipe info=$(info inside varref recipes) 68 | # this $() is executed (seeing the date) 69 | @echo varref recipe date=$(shell date) 70 | # this next $(error IS hit) (this comment is valid but Vim highlights as error) 71 | @echo varref recipe info=$(error inside varref recipes) 72 | 73 | end-of-varrefs=yeah this ends varrefs rule for sure 74 | 75 | 76 | spaces-in-weird-places : ; @echo = spaces-in-weird-places=$@ 77 | 78 | # the next line starts with a raw . Is valid. (starts empty shell? ignored?) 79 | 80 | 81 | # the next line is . Is valid. (ignored) 82 | 83 | 84 | # the next line starts with a raw . Is valid. (Sent to shell) 85 | foo=bar printenv "foo" 86 | 87 | @echo I am still inside $@ 88 | 89 | # the next line is . Is valid. The tabs are eaten. 90 | @echo tab tab 91 | 92 | # the next line is . Is valid. The tabs/spaces are eaten. 93 | @echo tab space tab 94 | 95 | # the next line is . Is valid. The tabs/spaces are eaten. 96 | @echo tab space tab space space space 97 | 98 | # the next line is has trailing s and s; can see the whitespace on a hexdump of this output 99 | # the trailing whitespace is preserved 100 | @echo I have trailing whitespacespaces 101 | 102 | # this line ends the recipe; note can be interpretted as either an assignment or a shell recipe 103 | foo=bar printenv "foo" 104 | $(info = bar printenv "foo"=$(foo)) 105 | 106 | whitespace-error : ; @echo $@ 107 | # the next line is then the echo 108 | # error "missing separator" -- make is expecting a statement (rule|assign|directive) 109 | # @echo space tab 110 | 111 | # error "missing separator" 112 | #foo bar baz 113 | 114 | ifdef foo 115 | endif 116 | 117 | # the comment should be sent to the shell 118 | comment-one-liner : ; # this is a comment 119 | 120 | # Need to avoid confusion between recipe trailing rule with ';' and the recipes 121 | # on the next line 122 | # This is not valid. 123 | #weird-semicolon: 124 | #; @echo $@ 125 | 126 | # tab before the @echo -- works 127 | single-line-with-tab : ; @echo $@ 128 | 129 | -------------------------------------------------------------------------------- /tests/define.mk: -------------------------------------------------------------------------------- 1 | # working with multi-line variables 2 | # davep 08-oct-2014 3 | 4 | bar=baz 5 | 6 | define foo = 7 | endef # foo foo foo 8 | 9 | # The following are from the GNU Make manual 10 | define two-lines := 11 | @echo two-lines foo 12 | @echo two-lines $(bar) 13 | endef 14 | 15 | define run-yacc 16 | yacc $(firstword $^) 17 | mv y.tab.c $@ 18 | endef 19 | 20 | define frobnicate = 21 | @echo "frobnicating target $@" 22 | frob-step-1 $< -o $@-step-1 23 | frob-step-2 $@-step-1 -o $@ 24 | endef 25 | 26 | # TODO override not implemented yet 27 | #override define two-lines = 28 | #@echo foo 29 | #@echo bar=$(bar) 30 | #endef 31 | # end of GNU Make copy/paste 32 | 33 | # well this makes things more difficult 34 | # valid 35 | define=not really define 36 | $(info $$define=$(define)) 37 | 38 | # not valid (whitespace triggers make's parser?) 39 | #define = define 40 | 41 | # not valid (again whitespace) 42 | #define : ; @echo $@ 43 | 44 | # $(info) will display multi-line variables 45 | $(info two-lines=$(two-lines)) 46 | #$(info $$frobnicate=em$(frobnicate)pty) 47 | 48 | $(info $(words $(value two-lines))) 49 | 50 | define crap = 51 | this is 52 | a 53 | load of crap 54 | that won't pass 55 | muster 56 | as a makefile 57 | endef 58 | 59 | ifeq ("$(crap)","") 60 | $(info must be make 3.81) 61 | endif 62 | 63 | ifneq ("$(crap)","") 64 | $(info must be make > 3.81) 65 | endif 66 | 67 | ifneq ("$(crap =)","") 68 | $(info must be make 3.81) 69 | endif 70 | 71 | ifeq ("$(crap =)","") 72 | $(info must be make > 3.81) 73 | endif 74 | 75 | define no-equal-sign 76 | foo foo foo 77 | endef 78 | 79 | ifneq ("$(no-equal-sign)","foo foo foo") 80 | $(error foo foo foo) 81 | endif 82 | 83 | #define nested = 84 | #blah blah blah blah define blah = 85 | #define nested-inner = 86 | # more blah more blah more blah 87 | # endef # foo foo foo 88 | #endef #foofoofoofoo 89 | 90 | # unterminated variable ref but only if eval'd 91 | define invalid 92 | $( 93 | endef 94 | 95 | # "unterminated variable ref" immediately because 96 | # simply expanded variable (contents parsed before use) 97 | #define also-invalid := 98 | #$( 99 | #endef 100 | 101 | define alphabet 102 | a:=a 103 | b:=b 104 | c:=c 105 | endef 106 | 107 | # error "empty variable name" 108 | #$(alphabet) 109 | 110 | # but this will exec the contents 111 | #$(eval $(alphabet)) 112 | 113 | # but this will exec the contents 114 | #$(eval $(alphabet)) 115 | $(info a=$a b=$b c=$c) 116 | 117 | # bare define is an error 118 | #define 119 | 120 | define car 121 | $(firstword $(1)) 122 | endef 123 | 124 | define cdr 125 | $(wordlist 2,$(words $(1)),$(1)) 126 | endef 127 | 128 | define mk2py 129 | $(foreach pyname,\ 130 | $(patsubst %.mk,%.py,$(1)),\ 131 | $(shell echo python tests/$(pyname))\ 132 | ) 133 | endef 134 | 135 | $(info $(call mk2py,a.mk b.mk c.mk d.mk)) 136 | $(info $(words $(value mk2py))) 137 | 138 | a := $(call car,$(call car,1,2,3,4,5,6)) 139 | $(info car=$a) 140 | 141 | $(info words=$(words 1 2 3 4 5 6 7)) 142 | a := $(call cdr,1 2 3 4 5 6) 143 | $(info cdr=$a) 144 | a := $(call cdr,$(call cdr,1 2 3 4 5 6)) 145 | $(info cdr=$a) 146 | 147 | # override previous bar so two-lines should now have qux 148 | bar=qux 149 | 150 | define shell_example != 151 | echo `date +%N` 152 | echo bar 153 | endef 154 | # output should be identical (only evaluated once) 155 | $(info 1 shell_example=$(shell_example)) 156 | $(info 2 shell_example=$(shell_example)) 157 | 158 | define silly_example := 159 | FOO:=foo 160 | BAR:=bar 161 | endef 162 | ifdef FOO 163 | $(error dave code is stupid) 164 | endif 165 | 166 | $(eval $(silly_example)) 167 | $(info FOO=$(FOO) BAR=$(BAR)) 168 | 169 | ifneq ($(FOO),foo) 170 | $(error FOO missing) 171 | endif 172 | 173 | define shell_example != 174 | echo `date +%N` 175 | echo bar 176 | endef 177 | 178 | $(info $(shell_example)) 179 | $(info $(shell_example)) 180 | 181 | @:;@: 182 | 183 | .PHONY: all 184 | all : 185 | $(call two-lines) 186 | # @echo $@ 187 | # @echo $(two-lines) 188 | # $(run-yacc) 189 | 190 | # valid (a rule) 191 | define: ; @echo running rule $@ 192 | 193 | -------------------------------------------------------------------------------- /tests/test_recipes.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | 3 | # whitebox test parsing a block of recipes 4 | 5 | from pymake import pymake 6 | from pymake.scanner import ScannerIterator 7 | import pymake.parser as parser 8 | import pymake.source as source 9 | import pymake.symbol as symbol 10 | import pymake.symtable as symtable 11 | from pymake.constants import backslash 12 | import pymake.vline as vline 13 | 14 | def parse_string(s): 15 | src = source.SourceString(s) 16 | src.load() 17 | line_scanner = ScannerIterator(src.file_lines, src.name) 18 | vline_iter = vline.get_vline(src.name, line_scanner) 19 | statement_list = [v for v in pymake.parse_vline(vline_iter)] 20 | 21 | assert not line_scanner.remain() 22 | return statement_list 23 | 24 | def make_recipelist(s): 25 | # everything in 's' should be a recipe 26 | statement_list = parse_string(s) 27 | return symbol.RecipeList(statement_list) 28 | 29 | def test_parse_recipes_simple(): 30 | s = """\ 31 | @echo foo 32 | @echo bar 33 | @echo baz 34 | """ 35 | recipe_list = make_recipelist(s) 36 | assert len(recipe_list)==3 37 | 38 | symbol_table = symtable.SymbolTable() 39 | for recipe in recipe_list: 40 | s = recipe.eval(symbol_table) 41 | assert s.startswith("@echo ") 42 | 43 | def test_parse_recipes_comments(): 44 | s = """\ 45 | @echo foo 46 | # this is a makefile comment 47 | @echo bar 48 | # this is a shell comment 49 | @echo baz 50 | """ 51 | recipe_list = make_recipelist(s) 52 | 53 | # the makefile comment should be discarded but the shell comment is preserved 54 | assert len(recipe_list)==4 55 | 56 | expect_list = ( "@echo foo", "@echo bar", "# this is a shell comment", "@echo baz" ) 57 | symbol_table = symtable.SymbolTable() 58 | for (recipe, expect_str) in zip(recipe_list, expect_list): 59 | s = recipe.eval(symbol_table) 60 | assert s == expect_str 61 | 62 | def test_parse_recipes_backslashes(): 63 | # from the GNU Make manual 64 | s = """\ 65 | @echo no\\ 66 | space 67 | @echo no\\ 68 | space 69 | @echo one \\ 70 | space 71 | @echo one\\ 72 | space 73 | """ 74 | recipe_list = make_recipelist(s) 75 | 76 | assert len(recipe_list)==4 77 | symbol_table = symtable.SymbolTable() 78 | for recipe in recipe_list: 79 | s = recipe.eval(symbol_table) 80 | # backslash is preserved 81 | p = s.index(backslash) 82 | assert s.startswith("@echo") 83 | assert s.endswith("space") 84 | 85 | def test_parse_end_of_recipes(): 86 | s = """\ 87 | @echo foo 88 | @echo bar 89 | 90 | $(info this should be end of recipes) 91 | """ 92 | statement_list = parse_string(s) 93 | recipe_list = symbol.RecipeList(statement_list[0:2]) 94 | 95 | # last statement should be an Expression 96 | last = statement_list[-1] 97 | assert isinstance(last, symbol.Expression) 98 | assert str(last) == 'Expression([Info([Literal("this should be end of recipes")])])' 99 | 100 | def test_trailing_recipes(): 101 | # handle rules that have recipes on the same line as the rule 102 | s = """\ 103 | foo: ; @echo foo 104 | @echo bar 105 | @echo baz 106 | """ 107 | statement_list = parse_string(s) 108 | assert len(statement_list)==3 109 | rule = statement_list[0] 110 | rule.add_recipe(statement_list[1]) 111 | rule.add_recipe(statement_list[2]) 112 | 113 | symbol_table = symtable.SymbolTable() 114 | 115 | expect_list = ( "@echo foo", "@echo bar", "@echo baz" ) 116 | symbol_table = symtable.SymbolTable() 117 | for (recipe, expect_str) in zip(rule.recipe_list, expect_list): 118 | s = recipe.eval(symbol_table) 119 | assert s == expect_str 120 | 121 | def test_recipe_ifdef_block(): 122 | s = """\ 123 | @echo foo? 124 | ifdef FOO 125 | @echo hello from foo! 126 | endif # FOO 127 | @echo bar 128 | """ 129 | statement_list = parse_string(s) 130 | assert isinstance(statement_list[0], symbol.Recipe) 131 | assert isinstance(statement_list[2], symbol.Recipe) 132 | 133 | # TODO add eval of the ifdef block to peek at the Recipe within 134 | -------------------------------------------------------------------------------- /tests/test_include.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | 3 | import os 4 | import tempfile 5 | 6 | import pytest 7 | 8 | import run 9 | 10 | # Jump through some strange hoops to write two temporary files. 11 | # GNU make allows multiple arguments to the include directive. 12 | def run_two_files(s): 13 | with tempfile.NamedTemporaryFile(buffering=0, delete=os.name != 'nt') as outfile1: 14 | with tempfile.NamedTemporaryFile(buffering=0, delete=os.name != 'nt') as outfile2: 15 | outfile1.write(b"$(info hello from outfile1)\n") 16 | outfile2.write(b"$(info hello from outfile2)\n") 17 | makefile_s = s % (outfile1.name, outfile2.name) 18 | 19 | try: 20 | gnumake_stdout = run.gnumake_string(makefile_s) 21 | print("gnu make stdout=\"%s\"" % gnumake_stdout) 22 | pymake_stdout = run.pymake_string(makefile_s) 23 | print("pymake stdout=\"%s\"" % pymake_stdout) 24 | 25 | assert gnumake_stdout==pymake_stdout, (gnumake_stdout, pymake_stdout) 26 | finally: 27 | if os.name == 'nt': 28 | outfile1.close() 29 | outfile2.close() 30 | os.remove(outfile1.name) 31 | os.remove(outfile2.name) 32 | 33 | 34 | def test1(): 35 | s = """ 36 | include /dev/null 37 | @:;@: 38 | """ 39 | run.simple_test(s) 40 | 41 | def test_leading_tab(): 42 | # GNU make will allow a leading an include statement 43 | s = """ 44 | include /dev/null 45 | @:;@: 46 | """ 47 | run.simple_test(s) 48 | 49 | def test_sinclude(): 50 | s = """ 51 | sinclude /path/does/not/exist 52 | @:;@: 53 | """ 54 | run.simple_test(s) 55 | 56 | def test_dash_include(): 57 | s = """ 58 | -include /path/does/not/exist 59 | @:;@: 60 | """ 61 | run.simple_test(s) 62 | 63 | def test_include_assignment(): 64 | s = """ 65 | include:=/path/does/not/exist 66 | ifneq ($(include),/path/does/not/exist) 67 | $(error fail) 68 | endif 69 | @:;@: 70 | """ 71 | run.simple_test(s) 72 | 73 | def test_whitespace(): 74 | s = """ 75 | include /dev/null 76 | @:;@: 77 | """ 78 | run.simple_test(s) 79 | 80 | def test_include_fail(): 81 | s = """ 82 | include /path/does/not/exist 83 | @:;@: 84 | """ 85 | run.gnumake_should_fail(s) 86 | run.pymake_should_fail(s) 87 | 88 | def test_include_two_files(): 89 | s = """ 90 | include %s %s 91 | @:;@: 92 | """ 93 | run_two_files(s) 94 | 95 | def test_include_two_files_tab(): 96 | # separate filenames by tab(s) 97 | s = """ 98 | include %s %s 99 | @:;@: 100 | """ 101 | run_two_files(s) 102 | 103 | def test_include_varref(): 104 | s = """ 105 | FILENAME:=/dev/null 106 | include ${FILENAME} 107 | @:;@: 108 | """ 109 | run.simple_test(s) 110 | 111 | def test_include_varref(): 112 | s = """ 113 | FILENAME:=/dev/null 114 | include ${FILENAME} 115 | @:;@: 116 | """ 117 | run.simple_test(s) 118 | 119 | def test_wildcard(): 120 | s = """ 121 | include $(wildcard /dev/null) 122 | @:;@: 123 | """ 124 | run.simple_test(s) 125 | 126 | @pytest.mark.skip(reason="FIXME broken in pymake") 127 | def test_generated_makefile(): 128 | # "Once it has finished reading makefiles, make will try to remake any that are 129 | # out of date or don’t exist. See Section 3.5 [How Makefiles Are Remade], page 130 | # 14. Only after it has tried to find a way to remake a makefile and failed, 131 | # will make diagnose the missing makefile as a fatal error." 132 | s = """ 133 | include {0} 134 | 135 | {0}: 136 | touch {0} 137 | 138 | @:;@: 139 | """ 140 | include_name = "tst.mk" 141 | makefile_s = s.format(include_name) 142 | 143 | with tempfile.TemporaryDirectory() as outdirname: 144 | filename = os.path.join(outdirname, include_name) 145 | with open(filename,"wb", buffering=0) as outfile: 146 | outfile.write("$(info hello from {})".format(include_name).encode("utf8")) 147 | 148 | gnumake_stdout = run.gnumake_string(makefile_s) 149 | print("gnu make stdout=\"%s\"" % gnumake_stdout) 150 | 151 | pymake_stdout = run.pymake_string(makefile_s) 152 | print("pymake stdout=\"%s\"" % gnumake_stdout) 153 | 154 | -------------------------------------------------------------------------------- /tests/test_wildcard.py: -------------------------------------------------------------------------------- 1 | from pymake.wildcard import split_percent, wildcard_match, wildcard_replace 2 | 3 | def test_split(): 4 | p = split_percent("hello.c") 5 | print(p) 6 | assert p is None 7 | 8 | p = split_percent("%.c") 9 | print(p) 10 | assert p and p[0] == '' and p[1] == '.c' 11 | 12 | p = split_percent("hello.%") 13 | print(p) 14 | assert p and p[0]=='hello.' and p[1] == '' 15 | 16 | p = split_percent("hello%.c") 17 | print(p) 18 | assert p and p[0]=='hello' and p[1] == '.c' 19 | 20 | p = split_percent("hello\\%.c") 21 | print(p) 22 | assert p is None 23 | 24 | # the \\\\ becomes \\ becomes a literal backslash 25 | p = split_percent("hello\\\\%.c") 26 | print(p) 27 | assert p and p[0]=='hello\\\\' and p[1]=='.c' 28 | 29 | p = split_percent("hello\\%there%.c") 30 | print(p) 31 | assert p and p[0]=='hello\\%there' and p[1]=='.c' 32 | 33 | p = split_percent("hello\\%there%.c") 34 | print(p) 35 | assert p and p[0]=='hello\\%there' and p[1]=='.c' 36 | 37 | p = split_percent("hello\\\\%") 38 | print(p) 39 | assert p and p[0]=='hello\\\\' and p[1]=='' 40 | 41 | p = split_percent("\\\\%") 42 | print(p) 43 | assert p and p[0]=='\\\\' and p[1]=='' 44 | 45 | # ignore 2nd % 46 | p = split_percent("\\\\%%.c") 47 | print(p) 48 | assert p and p[0]=='\\\\' and p[1]=='%.c' 49 | 50 | # literal % (no % to split around) 51 | p = split_percent("\\%") 52 | print(p) 53 | assert p is None 54 | # assert p and p[0]=='\\%' and p[1]=='' 55 | 56 | # literal % followed by wildcard % 57 | p = split_percent("\\%%.c") 58 | print(p) 59 | assert p and p[0]=='\\%' and p[1] == '.c' 60 | 61 | def test_match(): 62 | matches = wildcard_match("hello.c", ("hello.c",)) 63 | assert matches and matches == ["hello.c"] 64 | 65 | matches = wildcard_match("hello.c", ("there.c",)) 66 | assert len(matches)==0 67 | 68 | matches = wildcard_match("%.c", ("hello.c",)) 69 | assert matches and matches == ["hello.c"] 70 | 71 | matches = wildcard_match("hello.c", ("hello.c", "there.c", "all.c", "you.c", "rabbits.c")) 72 | assert matches and matches == ["hello.c"] 73 | 74 | matches = wildcard_match("%.c", ("hello.c", "there.c", "all.c", "you.c", "rabbits.c")) 75 | assert matches == ["hello.c", "there.c", "all.c", "you.c", "rabbits.c"] 76 | 77 | matches = wildcard_match("hello.%", ("hello.c", "there.c", "all.c", "you.c", "rabbits.c")) 78 | assert matches == ["hello.c"] 79 | 80 | # flag = wildcard_match("hello.c", "%.c") 81 | # assert flag 82 | 83 | def test_replace(): 84 | filenames = wildcard_replace("%.c", "%.o", ("hello.c",)) 85 | assert filenames[0]=="hello.o" 86 | 87 | # weird ; no basename 88 | new = wildcard_replace("%.c", "%.o", ["foo.c", ".c"]) 89 | print(new) 90 | assert new==["foo.o", ".o"] 91 | 92 | # nothing should change 93 | new = wildcard_replace("%.c", "%.o", ["foo.S", "bar.S"]) 94 | print(new) 95 | assert new == ["foo.S", "bar.S"] 96 | 97 | new = wildcard_replace("abc%", "xyz%", ["abcdefg", "abcdqrst",]) 98 | print(new) 99 | assert new == ["xyzdefg", "xyzdqrst"] 100 | 101 | new = wildcard_replace("abc%", "xyz%123", ["abcdefg", "abcdqrst",]) 102 | print(new) 103 | assert new == ["xyzdefg123", "xyzdqrst123"] 104 | 105 | new = wildcard_replace("%", "xyz%123", ["abcdef", "abcdqrst",]) 106 | print(new) 107 | assert new == ["xyzabcdef123", "xyzabcdqrst123"] 108 | 109 | # no matches, nothing changed 110 | # new = wildcard_replace("foo", "bar", ["abcdef", "tuvwxyz",]) 111 | # print(new) 112 | # assert new == ["abcdef", "tuvwxyz"] 113 | 114 | # no wildcards 115 | # new = wildcard_replace("foo", "bar", ["foo", "bar", "baz"]) 116 | # print(new) 117 | # assert new == ["bar", "bar", "baz"] 118 | 119 | # new = wildcard_replace("f%", "bar", ["foo", "bar", "baz"]) 120 | # print(new) 121 | # assert new == ["bar", "bar", "baz"] 122 | 123 | # no wildcards in 2nd arg (everything replaced) 124 | # new = wildcard_replace("%", "bar", ["foo", "bar", "baz"]) 125 | # print(new) 126 | # assert new == ["bar", "bar", "bar"] 127 | 128 | if __name__ == '__main__': 129 | # test_split() 130 | test_match() 131 | 132 | -------------------------------------------------------------------------------- /tests/test_seek_word.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | import pytest 5 | 6 | from pymake.constants import * 7 | from pymake.vline import VCharString 8 | from pymake.scanner import ScannerIterator 9 | from pymake.tokenizer import seek_word 10 | 11 | def test_export(): 12 | v = VCharString.from_string("export\n") 13 | vline = ScannerIterator(v, v.get_pos()[0]) 14 | w = seek_word(vline, directive) 15 | print(f"w={w}") 16 | assert str(w)=="export", w 17 | assert vline.is_empty() 18 | 19 | def test_substring(): 20 | v = VCharString.from_string("exportttt\n") 21 | vline = ScannerIterator(v, v.get_pos()[0]) 22 | w = seek_word(vline, directive) 23 | assert w is None, w 24 | assert vline.lookahead().get_pos()[1] == (0,0) 25 | 26 | def test_trailing_whitespace(): 27 | v = VCharString.from_string("export \n") 28 | vline = ScannerIterator(v, v.get_pos()[0]) 29 | w = seek_word(vline, directive) 30 | print(f"w={w}") 31 | assert str(w)=="export", w 32 | assert vline.is_empty() 33 | 34 | @pytest.mark.parametrize("d", directive) 35 | def test_all(d): 36 | v = VCharString.from_string(d) 37 | vline = ScannerIterator(v, v.get_pos()[0]) 38 | w = seek_word(vline, directive) 39 | print(f"d={d} w={w}") 40 | assert str(w)==d, (w,d) 41 | 42 | @pytest.mark.parametrize("d", directive) 43 | def test_all_whitespace(d): 44 | v = VCharString.from_string(" "+d+" ") 45 | vline = ScannerIterator(v, v.get_pos()[0]) 46 | w = seek_word(vline, directive) 47 | print(f"d={d} w={w}") 48 | assert str(w)==d, (w,d) 49 | 50 | @pytest.mark.parametrize("d", directive) 51 | def test_all_whitespace_tabs(d): 52 | v = VCharString.from_string("\t\t"+d+"\t\t") 53 | vline = ScannerIterator(v, v.get_pos()[0]) 54 | w = seek_word(vline, directive) 55 | print(f"d={d} w={w}") 56 | assert str(w)==d, (w,d) 57 | 58 | def test_export_statement(): 59 | v = VCharString.from_string("export SRC:=hello.c") 60 | vline = ScannerIterator(v, v.get_pos()[0]) 61 | w = seek_word(vline, directive) 62 | print(f"w={w}") 63 | assert str(w)=="export", w 64 | vchar = next(vline) 65 | assert vchar.char == 'S', vchar.char 66 | p = vchar.get_pos() 67 | assert p[1] == (0,7), p 68 | 69 | def test_multiple_modifiers(): 70 | v = VCharString.from_string("export override unexport private export unexport SRC:=hello.c") 71 | vline = ScannerIterator(v, v.get_pos()[0]) 72 | while True: 73 | w = seek_word(vline, directive) 74 | if not w: 75 | vchar = next(vline) 76 | assert vchar.char == 'S', vchar.char 77 | break 78 | else: 79 | print(f"w={w}") 80 | assert str(w) in directive, w 81 | 82 | def test_case_sensitivity(): 83 | v = VCharString.from_string("EXPORT SRC:=hello.c") 84 | vline = ScannerIterator(v, v.get_pos()[0]) 85 | w = seek_word(vline, directive) 86 | assert w is None, w 87 | assert vline.lookahead().get_pos()[1] == (0,0) 88 | 89 | def test_shuffled(): 90 | v = VCharString.from_string("pextor SRC:=hello.c") 91 | vline = ScannerIterator(v, v.get_pos()[0]) 92 | w = seek_word(vline, directive) 93 | assert w is None, w 94 | assert vline.lookahead().get_pos()[1] == (0,0) 95 | 96 | def test_comment(): 97 | v = VCharString.from_string("export#export everything") 98 | vline = ScannerIterator(v, v.get_pos()[0]) 99 | w = seek_word(vline, directive) 100 | assert str(w)=="export", w 101 | # should have consumed rest of line 102 | assert vline.is_empty() 103 | 104 | def test_comment_whitespace(): 105 | v = VCharString.from_string("export #export everything") 106 | vline = ScannerIterator(v, v.get_pos()[0]) 107 | w = seek_word(vline, directive) 108 | assert str(w)=="export", w 109 | # should have consumed rest of line 110 | assert vline.is_empty() 111 | 112 | def test_seek_delimited_word(): 113 | # should stop at non-matching character and be pointing at that char 114 | v = VCharString.from_string("hello world") 115 | vline = ScannerIterator(v, v.get_pos()[0]) 116 | w = seek_word(vline, set(("hello",))) 117 | assert str(w)=="hello", w 118 | vchar = next(vline) 119 | assert vchar.char == 'w', vchar.char 120 | 121 | def test_seek_nonwhitespace_delimited(): 122 | v = VCharString.from_string("export,CC:=GCC") 123 | vline = ScannerIterator(v, v.get_pos()[0]) 124 | w = seek_word(vline, directive) 125 | assert w is None, w 126 | assert vline.lookahead().get_pos()[1] == (0,0) 127 | 128 | -------------------------------------------------------------------------------- /tests/test_recipe_prefix.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import run 4 | 5 | # Test all the complex ways GNU Make handles recipe prefix (default ). 6 | # 7 | # GNU Make will test a character first, then seek directives, and if 8 | # a directive not found, then attempt to treat the line as a Recipe. If there's 9 | # no active Rule, then GNU Make flags an error. 10 | # See function eval() in src/read.c GNU Make 4.3 11 | 12 | def _verify(err, expect): 13 | # expect can be a string or a list (oops had to update code to handle 14 | # different error strings for shell output) 15 | if isinstance(expect,str): 16 | assert expect in err 17 | else: 18 | assert any((s in err for s in expect)) 19 | 20 | def run_fail_test(makefile, expect): 21 | err = run.gnumake_should_fail(makefile) 22 | print("err=",err) 23 | _verify(err, expect) 24 | 25 | err = run.pymake_should_fail(makefile) 26 | print("err=",err) 27 | _verify(err, expect) 28 | 29 | def run_test(makefile, expect): 30 | out = run.gnumake_string(makefile) 31 | print("out=",out) 32 | assert expect in out, out 33 | 34 | out = run.pymake_string(makefile) 35 | print("out=",out) 36 | assert expect in out 37 | 38 | def test1(): 39 | # normal everyday rule+recipe 40 | makefile=""" 41 | foo: 42 | @echo foo 43 | """ 44 | run_test(makefile, "foo") 45 | 46 | def test_define(): 47 | # define can have a leading tab 48 | # but endef cannot 49 | makefile=""" 50 | define DAVE 51 | 42 52 | endef 53 | foo: 54 | @echo $(DAVE) 55 | """ 56 | run_test(makefile, "42") 57 | 58 | def test_error_define_endef(): 59 | # define can have a leading tab 60 | # but endef cannot 61 | makefile=""" 62 | define DAVE 63 | 42 64 | endef 65 | foo: 66 | @echo $(DAVE) 67 | """ 68 | run_fail_test(makefile, "missing 'endef', unterminated 'define'") 69 | 70 | def test_var_assign_before_first_rule(): 71 | # variable assignment allowed as long as before the first rule 72 | makefile = """ 73 | a := 42 74 | foo: 75 | @echo $(a) 76 | """ 77 | run_test(makefile, "42") 78 | 79 | def test_leading_tab_before_rule(): 80 | makefile = """ 81 | $(info leading tab before any rules) 82 | @:;@: 83 | """ 84 | run_fail_test(makefile, "recipe commences before first target") 85 | 86 | def test_tab_comment(): 87 | makefile = """ 88 | # this is a comment with leading tab and is ignored 89 | foo: 90 | @echo foo 91 | """ 92 | run_test(makefile, "foo") 93 | 94 | def test_tab_tab_comment(): 95 | makefile = """ 96 | # this is a comment with leading tab tab and is ignored 97 | foo: 98 | @echo foo 99 | """ 100 | run_test(makefile, "foo") 101 | 102 | def test_tab_spaces_comment(): 103 | makefile = """ 104 | # this is a comment with leading tab and some spaces and is ignored 105 | foo: 106 | @echo foo 107 | """ 108 | run_test(makefile, "foo") 109 | 110 | def test_bare_tab(): 111 | # line with just a bare tab is ignored 112 | makefile = """ 113 | 114 | foo: 115 | @echo foo 116 | """ 117 | run_test(makefile, "foo") 118 | 119 | def test_recipe_missing_tab(): 120 | makefile = """ 121 | # no tab character on the recipe! 122 | foo: 123 | @echo foo 124 | """ 125 | run_fail_test(makefile, "missing separator") 126 | 127 | def test_tab_ifdef(): 128 | makefile = """ 129 | FOO=1 130 | ifdef FOO 131 | $(info FOO=$(FOO)) 132 | endif 133 | @:;@: 134 | """ 135 | run_test(makefile, "FOO=1") 136 | 137 | def test_recipe_with_ifdef_spaces(): 138 | # should work fine 139 | makefile = """ 140 | FOO:=1 141 | foo: 142 | ifdef FOO 143 | @echo foo 144 | endif 145 | """ 146 | run_test(makefile, "foo") 147 | 148 | def test_recipe_with_ifdef_tabs(): 149 | # the ifdef has a leading tab and we're in a Rule therefore the ifdef is 150 | # treated as a Recipe. 151 | # 152 | # will fail with 'ifdef: No such file or directory' 153 | makefile = """ 154 | FOO:=1 155 | 156 | foo: 157 | ifdef FOO 158 | @echo foo 159 | endif 160 | """ 161 | run_fail_test(makefile, ('No such file or directory', 'not found')) 162 | 163 | def test_tab_before_recipe(): 164 | makefile=""" 165 | FOO:=1 166 | 167 | # because we haven't seen a Recipe yet, this is treated as just a regular line. 168 | ifdef FOO 169 | $(info FOO=$(FOO)) 170 | endif 171 | 172 | # "rule without a target" for 173 | # "compatibility with SunOS 4 make" 174 | : foo 175 | @echo error\\\\! should not see this 176 | exit 1 177 | 178 | foo: 179 | ifdef FOO 180 | @echo foo 181 | endif 182 | """ 183 | run_test(makefile, "FOO=1\nfoo") 184 | 185 | -------------------------------------------------------------------------------- /tests/test_shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0 3 | 4 | import os 5 | import shutil 6 | import itertools 7 | 8 | import run 9 | import verify 10 | 11 | _debug = True 12 | 13 | def test_ignore_environment(): 14 | makefile=""" 15 | $(info SHELL=$(SHELL)) 16 | @:;@: 17 | """ 18 | p = run.gnumake_string(makefile,extra_env={"SHELL":"/bin/bash"}) 19 | assert "SHELL=/bin/sh" in p, p 20 | 21 | p = run.pymake_string(makefile,extra_env={"SHELL":"/bin/bash"}) 22 | assert "SHELL=/bin/sh" in p, p 23 | 24 | def test_default_variables(): 25 | makefile=""" 26 | $(info SHELL=$(SHELL)) 27 | $(info .SHELLFLAGS=$(.SHELLFLAGS)) 28 | @:;@: 29 | """ 30 | expect = ( 31 | "SHELL=/bin/sh", 32 | ".SHELLFLAGS=-c", 33 | ) 34 | p = run.gnumake_string(makefile) 35 | verify.compare_result_stdout(expect, p) 36 | 37 | p = run.pymake_string(makefile) 38 | verify.compare_result_stdout(expect, p) 39 | 40 | def test_empty_shell(): 41 | ls = shutil.which('ls') 42 | makefile=""" 43 | SHELL:= 44 | .SHELLFLAGS:= 45 | $(info $(shell %s)) 46 | @:;@: 47 | """ % ls 48 | 49 | # will launch e.g., /usr/bin/ls as argv[0] 50 | # which should happily succeed 51 | p = run.gnumake_string(makefile) 52 | # make sure we get something back 53 | assert p.strip(), p 54 | print("p=",p) 55 | 56 | p = run.pymake_string(makefile) 57 | 58 | def test_empty_shell_with_args(): 59 | ls = shutil.which('ls') 60 | makefile=""" 61 | SHELL:= 62 | .SHELLFLAGS:= 63 | $(info $(shell %s *.mk)) 64 | @:;@: 65 | """ % ls 66 | 67 | error = ("No such file or directory", "Command not found") 68 | # will launch .e.g, '/usr/bin/ls *.mk' as the complete command 69 | # argv[0] == '/usr/bin/ls *.mk' 70 | # which should error with 'No such file or directory' 71 | p = run.gnumake_string(makefile, flags=run.FLAG_OUTPUT_STDERR) 72 | # print("p=",p) 73 | assert "ls *.mk" in p, p 74 | assert any([e in p for e in error]) 75 | 76 | p = run.pymake_string(makefile, flags=run.FLAG_OUTPUT_STDERR) 77 | assert "ls *.mk" in p, p 78 | assert any([e in p for e in error]) 79 | 80 | def test_permission_denied(): 81 | # this makefile will try to literally exec /dev/zero which will fail with a 82 | # permission denied 83 | makefile=""" 84 | SHELL:= 85 | .SHELLFLAGS:= 86 | $(info $(shell /dev/zero)) 87 | @:;@: 88 | """ 89 | p = run.gnumake_string(makefile, flags=run.FLAG_OUTPUT_STDERR) 90 | print("p=",p) 91 | assert "/dev/zero" in p, p 92 | assert "Permission denied" in p, p 93 | 94 | p = run.pymake_string(makefile, flags=run.FLAG_OUTPUT_STDERR) 95 | print("p=",p) 96 | 97 | def test_shell_wildcards(): 98 | # GNU make will launch /usr/bin/cat directly 99 | makefile=""" 100 | $(info $(sort $(shell cat /etc/passwd))) 101 | @:;@: 102 | """ 103 | # GNU make will launch /bin/sh to call cat 104 | makefile=""" 105 | $(info $(sort $(shell cat /etc/pass??))) 106 | @:;@: 107 | """ 108 | # pymake will always use the shell. 109 | 110 | # TODO 111 | 112 | def test_shell_trailing_whitespace(): 113 | # spaces should be trimmed 114 | # the SHELL line below has leading and trailing whitespace 115 | makefile=""" 116 | SHELL:= /bin/sh 117 | $(info SHELL=$(SHELL)) 118 | $(info $(shell ls)) 119 | @:;@: 120 | """ 121 | p = run.gnumake_string(makefile) 122 | print("p=",p) 123 | 124 | p = run.pymake_string(makefile) 125 | print("p=",p) 126 | 127 | def test_perl(): 128 | makefile=""" 129 | SHELL:=perl 130 | .SHELLFLAGS:=-e 131 | $(info $(shell print('hello, $$(shell) perl'))) 132 | # let perl run recipes 133 | all: 134 | @print('hello, recipe perl') 135 | """ 136 | expect = ( "hello, $(shell) perl", 137 | "hello, recipe perl") 138 | 139 | p = run.gnumake_string(makefile) 140 | print("p=",p) 141 | verify.compare_result_stdout(expect, p) 142 | 143 | p = run.pymake_string(makefile) 144 | print("p=",p) 145 | verify.compare_result_stdout(expect, p) 146 | 147 | def test_multiword(): 148 | makefile=""" 149 | # "SHELL may be a multi-word command" says a comment in src/job.c 150 | SHELL:=perl -e 151 | .SHELLFLAGS:= 152 | $(info $(shell print('hello again, perl'))) 153 | @:;@: 154 | """ 155 | p = run.gnumake_string(makefile) 156 | print("p=",p) 157 | 158 | # p = run.pymake_string(makefile) 159 | # print("p=",p) 160 | 161 | def test_empty_shell(): 162 | makefile = """ 163 | foo=$(shell ) 164 | @:;@: 165 | """ 166 | p = run.gnumake_string(makefile) 167 | print("p=",p) 168 | 169 | p = run.pymake_string(makefile) 170 | print("p=",p) 171 | 172 | def test_shell_python(): 173 | makefile=""" 174 | """ 175 | # TODO 176 | -------------------------------------------------------------------------------- /pymake/error.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0 2 | # Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com 3 | 4 | import sys 5 | 6 | __all__ = [ "ParseError", 7 | "MakeError", 8 | "RecipeCommencesBeforeFirstTarget", 9 | "MissingSeparator", 10 | "InvalidFunctionArguments", 11 | "InvalidSyntaxInConditional", 12 | "EmptyVariableName", 13 | "NoMakefileFound", 14 | "InternalError", 15 | "MissingEndef", 16 | "RecursiveVariableError", 17 | 18 | "warning_message", 19 | "error_message", 20 | 21 | "exit_status", 22 | ] 23 | 24 | # from "EXIT STATUS" of make(1) 25 | exit_status = { 26 | "success" : 0, 27 | "rebuild-required" : 1, 28 | "error" : 2, # "errors were encountered" 29 | } 30 | 31 | # test/debug flags 32 | # assert() in ParseError() constructor TODO make this a command line arg 33 | assert_on_parse_error = False 34 | 35 | class MakeError(Exception): 36 | # base class of all pymake exceptions 37 | description = "(No description!)" # useful description of the error 38 | default_msg = "(no message)" 39 | 40 | def __init__(self,*args,**kwargs): 41 | super().__init__(*args) 42 | self.code = kwargs.get("code",None) 43 | self.pos = kwargs.get("pos", ("missing",(-1,-1))) 44 | self.filename = self.pos[0] 45 | self.msg = kwargs.get("msg") or self.default_msg 46 | if "moremsg" in kwargs: 47 | self.msg += "; %s" % kwargs["moremsg"] 48 | 49 | def get_pos(self): 50 | return self.pos 51 | 52 | def __str__(self): 53 | return "*** filename=\"{0}\" pos={1}: {2}".format( 54 | self.filename,self.pos[1],self.msg) 55 | # return "*** filename=\"{0}\" pos={1} src=\"{2}\": {3}".format( 56 | # self.filename,self.pos,str(self.code).strip(),self.msg) 57 | 58 | class ParseError(MakeError): 59 | def __init__(self, *args, **kwargs): 60 | super().__init__(*args, **kwargs) 61 | 62 | if assert_on_parse_error : 63 | # handy for stopping immediately to diagnose a parse error 64 | # (especially when the parse error is unexpected or wrong) 65 | assert 0 66 | 67 | class RecipeCommencesBeforeFirstTarget(ParseError): 68 | description = """\"Recipe Commmences Before First Target\" 69 | Usually a parse error. Make has gotten confused by a RECIPEPREFIX (by default, 70 | \\t (tab)) found at the start of line and thinks we've found a Recipe. 71 | TODO add more description here 72 | """ 73 | default_msg = "recipe commences before first target" 74 | 75 | class MissingSeparator(ParseError): 76 | description = """\"Missing Separator\" 77 | Usually a parse error. Make has found some text that 78 | doesn't successfully parse into a rule or an expression. 79 | - Can happen when a Recipe doesn't the proper recipe prefix (default \\t (tab)) 80 | - Can happen when a text transformation function "leaks" text into the parser 81 | where it should be captured by a variable. 82 | 83 | TODO add better+more description here 84 | """ 85 | default_msg = "missing separator" 86 | 87 | class InvalidFunctionArguments(ParseError): 88 | description = """Arguments to a function are incorrect.""" 89 | 90 | class InvalidSyntaxInConditional(ParseError): 91 | default_msg = "invalid syntax in conditional" 92 | 93 | class EmptyVariableName(ParseError): 94 | default_msg = "empty variable name" 95 | 96 | class NoMakefileFound(MakeError): 97 | default_msg = "No targets specified and no makefile found." 98 | 99 | class InternalError(MakeError): 100 | default_msg = "INTERNAL ERROR! Something went wrong inside pymake itself." 101 | 102 | class MissingEndef(ParseError): 103 | default_msg = "missing 'endef', unterminated 'define'" 104 | 105 | class RecursiveVariableError(MakeError): 106 | default_msg = "Recursive variable references itself eventually." 107 | 108 | #class VersionError(MakeError): 109 | # """Feature not in this version""" 110 | # pass 111 | 112 | def warning_message(pos, msg): 113 | # don't allow an empty message because it's super confusing 114 | assert msg 115 | 116 | if pos: 117 | print("%s %r warning: %s" % (pos[0], pos[1], msg), file=sys.stderr) 118 | else: 119 | print("(pos unknown): %s" % (msg,), file=sys.stderr) 120 | 121 | def error_message(pos, msg): 122 | # don't allow an empty message because it's super confusing 123 | assert msg 124 | 125 | if pos: 126 | print("%s %r: *** %s" % (pos[0], pos[1], msg), file=sys.stderr) 127 | else: 128 | print("*** %s" % msg, file=sys.stderr) 129 | 130 | -------------------------------------------------------------------------------- /tests/test_ifeq.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import run 4 | 5 | def test_nested_missing_endif(): 6 | s = """ 7 | ifeq (10,10) 8 | ifeq (a,b) 9 | 10 | endif 11 | @:;@: 12 | """ 13 | msg = run.pymake_should_fail(s) 14 | assert "missing endif" in msg 15 | 16 | def test_nested_skipped_missing_endif(): 17 | s = """ 18 | ifeq (10,11) 19 | ifeq (a,b 20 | 21 | endif 22 | @:;@: 23 | """ 24 | msg = run.pymake_should_fail(s) 25 | assert "missing endif" in msg 26 | 27 | def test_nested(): 28 | s = """ 29 | ifeq (10,10) 30 | ifeq (a,b) 31 | endif 32 | endif 33 | @:;@: 34 | """ 35 | run.pymake_string(s) 36 | 37 | def test_deep_nested(): 38 | s = """ 39 | ifeq (10,10) 40 | ifeq (11,11) 41 | ifeq (12,12) 42 | ifeq (13,13) 43 | ifeq (14,14) 44 | ifeq (15,15) 45 | ifeq (16,16) 46 | ifeq (17,17) 47 | ifeq (18,18) 48 | ifeq (19,19) 49 | ifeq (20,20) 50 | $(info 20) 51 | endif 52 | $(info 19) 53 | endif 54 | endif 55 | endif 56 | endif 57 | endif 58 | endif 59 | endif 60 | endif 61 | endif 62 | endif 63 | @:;@: 64 | """ 65 | run.pymake_string(s) 66 | 67 | def test_deep_nested_else(): 68 | s = """ 69 | ifeq (10,10) 70 | ifeq (11,11) 71 | ifeq (12,12) 72 | ifeq (13,13) 73 | ifeq (14,14) 74 | ifeq (15,15) 75 | ifeq (16,16) 76 | ifeq (17,17) 77 | ifeq (18,18) 78 | ifeq (19,19) 79 | ifeq (20,20) 80 | else 81 | endif 82 | else 83 | endif 84 | else 85 | endif 86 | else 87 | endif 88 | else 89 | endif 90 | else 91 | endif 92 | else 93 | endif 94 | else 95 | endif 96 | else 97 | endif 98 | else 99 | endif 100 | else 101 | endif 102 | @:;@: 103 | """ 104 | run.pymake_string(s) 105 | 106 | def test_nested_valid_inside(): 107 | s = """ 108 | ifeq (10,10) 109 | ifeq (a,a) 110 | $(info should see this) 111 | endif 112 | endif 113 | @:;@: 114 | """ 115 | run.pymake_string(s) 116 | 117 | def test_nested_invalid_inside(): 118 | # note the missing close ) on the inner expression 119 | s = """ 120 | ifeq (10,11) 121 | ifeq (a,a 122 | $(error should not see this) 123 | endif 124 | endif 125 | @:;@: 126 | """ 127 | run.pymake_string(s) 128 | 129 | def test_bare_endif(): 130 | s = """ 131 | endif 132 | @:;@: 133 | """ 134 | msg = run.pymake_should_fail(s) 135 | assert "extraneous 'endif'" in msg, msg 136 | 137 | # This is a weird corner case. Check for a directive that isn't 138 | # properly whitespace separated. GNU Make doesn't detect it as 139 | # a directive in conditional_line()-src/read.c so it falls 140 | # through to the catch-all error "missing separator". My parser 141 | # is dependent on whitespace so an improper directive will be 142 | # ignored until execute() stage. 143 | # ifeq'1' '1' <-- note missing whitespace after ifeq 144 | # endif 145 | @pytest.mark.skip(reason="FIXME missing whitespace after ifeq") 146 | def test_missing_whitespace_ifeq(): 147 | s = """ 148 | ifeq(a,b) 149 | endif 150 | @:;@: 151 | """ 152 | msg = run.pymake_should_fail(s) 153 | assert "missing endif" in msg 154 | 155 | def test_bare_ifdef(): 156 | s = """ 157 | ifeq 158 | @:;@: 159 | """ 160 | msg = run.pymake_should_fail(s) 161 | assert "invalid syntax in conditional" in msg 162 | 163 | def test_bare_ifdef_whitespace(): 164 | # NOTE! there are trailing whitespace after the ifdef 165 | s = """ 166 | ifeq 167 | @:;@: 168 | """ 169 | msg = run.pymake_should_fail(s) 170 | assert "invalid syntax in conditional" in msg 171 | 172 | def test_nested_evalutation(): 173 | s=""" 174 | ifneq (0,1) 175 | foo:=1 176 | ifneq ($(foo),1) 177 | $(error should not hit this) 178 | endif 179 | endif 180 | @:;@: 181 | """ 182 | run.pymake_string(s) 183 | 184 | --------------------------------------------------------------------------------