├── .gitignore ├── .hgignore ├── LICENSE ├── README ├── make.py ├── mkformat.py ├── mkparse.py ├── pymake ├── __init__.py ├── builtins.py ├── command.py ├── data.py ├── errors.py ├── functions.py ├── globrelative.py ├── implicit.py ├── parser.py ├── parserdata.py ├── process.py ├── util.py └── win32process.py └── tests ├── automatic-variables.mk ├── bad-command-continuation.mk ├── call.mk ├── cmd-stripdotslash.mk ├── cmdgoals.mk ├── commandmodifiers.mk ├── comment-parsing.mk ├── continuations-in-functions.mk ├── datatests.py ├── default-goal-set-first.mk ├── default-goal.mk ├── default-target.mk ├── default-target2.mk ├── define-directive.mk ├── depfailed.mk ├── depfailedj.mk ├── diamond-deps.mk ├── dotslash-dir.mk ├── dotslash-parse.mk ├── dotslash-phony.mk ├── dotslash.mk ├── doublecolon-exists.mk ├── doublecolon-priordeps.mk ├── doublecolon-remake.mk ├── dynamic-var.mk ├── empty-arg.mk ├── empty-command-semicolon.mk ├── empty-rule.mk ├── empty-with-deps.mk ├── env-var-append.mk ├── env-var-append2.mk ├── eof-continuation.mk ├── escape-chars.mk ├── escaped-continuation.mk ├── eval-duringexecute.mk ├── eval.mk ├── exit-code.mk ├── file-functions-symlinks.mk ├── file-functions.mk ├── foreach-local-variable.mk ├── formattingtests.py ├── func-refs.mk ├── functions.mk ├── functiontests.py ├── glob1.inc ├── glob2.inc ├── if-syntaxerr.mk ├── ifdefs-nesting.mk ├── ifdefs.mk ├── ignore-error.mk ├── implicit-chain.mk ├── implicit-dir.mk ├── implicit-terminal.mk ├── implicitsubdir.mk ├── include-dynamic.mk ├── include-file.inc ├── include-glob.mk ├── include-missing.mk ├── include-notfound.mk ├── include-optional-warning.mk ├── include-regen.mk ├── include-regen2.mk ├── include-regen3.mk ├── include-required-fails.mk ├── include-test.mk ├── includedeps-norebuild.mk ├── includedeps-sideeffects.mk ├── includedeps-stripdotslash.deps ├── includedeps-stripdotslash.mk ├── includedeps-variables.deps ├── includedeps-variables.mk ├── includedeps.deps ├── includedeps.mk ├── info.mk ├── justprint-native.mk ├── justprint.mk ├── keep-going-doublecolon.mk ├── keep-going-parallel.mk ├── keep-going.mk ├── line-continuations.mk ├── link-search.mk ├── makeflags.mk ├── matchany.mk ├── matchany2.mk ├── matchany3.mk ├── mkdir-fail.mk ├── mkdir.mk ├── multiple-rules-prerequisite-merge.mk ├── native-command-delay-load.mk ├── native-command-raise.mk ├── native-command-return-fail1.mk ├── native-command-return-fail2.mk ├── native-command-return.mk ├── native-command-shell-glob.mk ├── native-command-sys-exit-fail1.mk ├── native-command-sys-exit-fail2.mk ├── native-command-sys-exit.mk ├── native-environment.mk ├── native-pycommandpath-sep.mk ├── native-pycommandpath.mk ├── native-simple.mk ├── native-touch.mk ├── newlines.mk ├── no-remake.mk ├── nosuchfile.mk ├── notargets.mk ├── notparallel.mk ├── oneline-command-continuations.mk ├── override-propagate.mk ├── parallel-dep-resolution.mk ├── parallel-dep-resolution2.mk ├── parallel-native.mk ├── parallel-simple.mk ├── parallel-submake.mk ├── parallel-toserial.mk ├── parallel-waiting.mk ├── parentheses.mk ├── parsertests.py ├── path-length.mk ├── pathdir ├── pathtest ├── pathtest.exe └── src │ ├── Makefile │ └── pathtest.cpp ├── patsubst.mk ├── phony.mk ├── pycmd.py ├── recursive-set.mk ├── recursive-set2.mk ├── remake-mtime.mk ├── rm-fail.mk ├── rm.mk ├── runtests.py ├── serial-dep-resolution.mk ├── serial-doublecolon-execution.mk ├── serial-rule-execution.mk ├── serial-rule-execution2.mk ├── serial-toparallel.mk ├── shellfunc.mk ├── simple-makeflags.mk ├── sort.mk ├── specified-target.mk ├── static-pattern.mk ├── static-pattern2.mk ├── subdir ├── delayload.py ├── pymod.py └── testmodule.py ├── submake-path.makefile2 ├── submake-path.mk ├── submake.makefile2 ├── submake.mk ├── subprocess-path.mk ├── tab-intro.mk ├── target-specific.mk ├── unexport.mk ├── unexport.submk ├── unterminated-dollar.mk ├── var-change-flavor.mk ├── var-commandline.mk ├── var-overrides.mk ├── var-ref.mk ├── var-set.mk ├── var-substitutions.mk ├── vpath-directive-dynamic.mk ├── vpath-directive.mk ├── vpath.mk ├── vpath2.mk ├── wildcards.mk └── windows-paths.mk /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tests/*makelog 3 | .ropeproject/ 4 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | \.pyc$ 2 | \.pyo$ 3 | tests/.*.(py|g)makelog$ 4 | ~$ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 The Mozilla Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | INTRODUCTION 2 | 3 | make.py (and the pymake modules that support it) are an implementation of the make tool 4 | which are mostly compatible with makefiles written for GNU make. 5 | 6 | PURPOSE 7 | 8 | The Mozilla project inspired this tool with several goals: 9 | 10 | * Improve build speeds, especially on Windows. This can be done by reducing the total number 11 | of processes that are launched, especially MSYS shell processes which are expensive. 12 | 13 | * Allow writing some complicated build logic directly in Python instead of in shell. 14 | 15 | * Allow computing dependencies for special targets, such as members within ZIP files. 16 | 17 | * Enable experiments with build system. By writing a makefile parser, we can experiment 18 | with converting in-tree makefiles to another build system, such as SCons, waf, ant, ...insert 19 | your favorite build tool here. Or we could experiment along the lines of makepp, keeping 20 | our existing makefiles, but change the engine to build a global dependency graph. 21 | 22 | KNOWN INCOMPATIBILITIES 23 | 24 | * Order-only prerequisites are not yet supported 25 | 26 | * Secondary expansion is not yet supported. 27 | 28 | * Target-specific variables behave differently than in GNU make: in pymake, the target-specific 29 | variable only applies to the specific target that is mentioned, and does not apply recursively 30 | to all dependencies which are remade. This is an intentional change: the behavior of GNU make 31 | is neither deterministic nor intuitive. 32 | 33 | * $(eval) is only supported during the parse phase. Any attempt to recursively expand 34 | an $(eval) function during command execution will fail. This is an intentional incompatibility. 35 | 36 | * There is a subtle difference in execution order that can cause unexpected changes in the 37 | following circumstance: 38 | ** A file `foo.c` exists on the VPATH 39 | ** A rule for `foo.c` exists with a dependency on `tool` and no commands 40 | ** `tool` is remade for some other reason earlier in the file 41 | In this case, pymake resets the VPATH of `foo.c`, while GNU make does not. This shouldn't 42 | happen in the real world, since a target found on the VPATH without commands is silly. But 43 | mozilla/js/src happens to have a rule, which I'm patching. 44 | 45 | * pymake does not implement any of the builtin implicit rules or the related variables. Mozilla 46 | only cares because pymake doesn't implicitly define $(RM), which I'm also fixing in the Mozilla 47 | code. 48 | 49 | ISSUES 50 | 51 | * Speed is a problem. 52 | 53 | FUTURE WORK 54 | 55 | * implement a new type of command which is implemented in python. This would allow us 56 | to replace the current `nsinstall` binary (and execution costs for the shell and binary) with an 57 | in-process python solution. 58 | 59 | AUTHOR 60 | 61 | Initial code was written by Benjamin Smedberg . For future releases see 62 | http://benjamin.smedbergs.us/pymake/ 63 | 64 | See the LICENSE file for license information (MIT license) 65 | -------------------------------------------------------------------------------- /make.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | make.py 5 | 6 | A drop-in or mostly drop-in replacement for GNU make. 7 | """ 8 | 9 | import sys, os 10 | import pymake.command, pymake.process 11 | 12 | import gc 13 | 14 | if __name__ == '__main__': 15 | if sys.version_info < (3,0): 16 | sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) 17 | sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0) 18 | else: 19 | # Unbuffered text I/O is not allowed in Python 3. 20 | sys.stdout = os.fdopen(sys.stdout.fileno(), 'w') 21 | sys.stderr = os.fdopen(sys.stderr.fileno(), 'w') 22 | 23 | gc.disable() 24 | 25 | pymake.command.main(sys.argv[1:], os.environ, os.getcwd(), cb=sys.exit) 26 | pymake.process.ParallelContext.spin() 27 | assert False, "Not reached" 28 | -------------------------------------------------------------------------------- /mkformat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import pymake.parser 5 | 6 | filename = sys.argv[1] 7 | source = None 8 | 9 | with open(filename, 'rU') as fh: 10 | source = fh.read() 11 | 12 | statements = pymake.parser.parsestring(source, filename) 13 | print(statements.to_source()) 14 | -------------------------------------------------------------------------------- /mkparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import pymake.parser 5 | 6 | for f in sys.argv[1:]: 7 | print("Parsing %s" % f) 8 | fd = open(f, 'rU') 9 | s = fd.read() 10 | fd.close() 11 | stmts = pymake.parser.parsestring(s, f) 12 | print(stmts) 13 | -------------------------------------------------------------------------------- /pymake/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/pymake/034ae9ea5b726e03647d049147c5dbf688e94aaf/pymake/__init__.py -------------------------------------------------------------------------------- /pymake/builtins.py: -------------------------------------------------------------------------------- 1 | # Basic commands implemented in Python 2 | import errno, sys, os, shutil, time 3 | from getopt import getopt, GetoptError 4 | 5 | from pymake import errors 6 | 7 | __all__ = ["mkdir", "rm", "sleep", "touch"] 8 | 9 | def mkdir(args): 10 | """ 11 | Emulate some of the behavior of mkdir(1). 12 | Only supports the -p (--parents) argument. 13 | """ 14 | try: 15 | opts, args = getopt(args, "p", ["parents"]) 16 | except GetoptError as e: 17 | raise errors.PythonError("mkdir: %s" % e, 1) 18 | parents = False 19 | for o, a in opts: 20 | if o in ('-p', '--parents'): 21 | parents = True 22 | for f in args: 23 | try: 24 | if parents: 25 | os.makedirs(f) 26 | else: 27 | os.mkdir(f) 28 | except OSError as e: 29 | if e.errno == errno.EEXIST and parents: 30 | pass 31 | else: 32 | raise errors.PythonError("mkdir: %s" % e, 1) 33 | 34 | def rm(args): 35 | """ 36 | Emulate most of the behavior of rm(1). 37 | Only supports the -r (--recursive) and -f (--force) arguments. 38 | """ 39 | try: 40 | opts, args = getopt(args, "rRf", ["force", "recursive"]) 41 | except GetoptError as e: 42 | raise errors.PythonError("rm: %s" % e, 1) 43 | force = False 44 | recursive = False 45 | for o, a in opts: 46 | if o in ('-f', '--force'): 47 | force = True 48 | elif o in ('-r', '-R', '--recursive'): 49 | recursive = True 50 | for f in args: 51 | if os.path.isdir(f): 52 | if not recursive: 53 | raise errors.PythonError("rm: cannot remove '%s': Is a directory" % f, 1) 54 | else: 55 | shutil.rmtree(f, force) 56 | elif os.path.exists(f): 57 | try: 58 | os.unlink(f) 59 | except: 60 | if not force: 61 | raise errors.PythonError("rm: failed to remove '%s': %s" % (f, sys.exc_info()[0]), 1) 62 | elif not force: 63 | raise errors.PythonError("rm: cannot remove '%s': No such file or directory" % f, 1) 64 | 65 | def sleep(args): 66 | """ 67 | Emulate the behavior of sleep(1). 68 | """ 69 | total = 0 70 | values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400} 71 | for a in args: 72 | multiplier = 1 73 | for k, v in values.items(): 74 | if a.endswith(k): 75 | a = a[:-1] 76 | multiplier = v 77 | break 78 | try: 79 | f = float(a) 80 | total += f * multiplier 81 | except ValueError: 82 | raise errors.PythonError("sleep: invalid time interval '%s'" % a, 1) 83 | time.sleep(total) 84 | 85 | def touch(args): 86 | """ 87 | Emulate the behavior of touch(1). 88 | """ 89 | try: 90 | opts, args = getopt(args, "t:") 91 | except GetoptError as e: 92 | raise errors.PythonError("touch: %s" % e, 1) 93 | opts = dict(opts) 94 | times = None 95 | if '-t' in opts: 96 | import re 97 | from time import mktime, localtime 98 | m = re.match('^(?P(?:\d\d)?\d\d)?(?P\d\d)(?P\d\d)(?P\d\d)(?P\d\d)(?:\.(?P\d\d))?$', opts['-t']) 99 | if not m: 100 | raise errors.PythonError("touch: invalid date format '%s'" % opts['-t'], 1) 101 | def normalized_field(m, f): 102 | if f == 'Y': 103 | if m.group(f) is None: 104 | return localtime()[0] 105 | y = int(m.group(f)) 106 | if y < 69: 107 | y += 2000 108 | elif y < 100: 109 | y += 1900 110 | return y 111 | if m.group(f) is None: 112 | return localtime()[0] if f == 'Y' else 0 113 | return int(m.group(f)) 114 | time = [normalized_field(m, f) for f in ['Y', 'M', 'D', 'h', 'm', 's']] + [0, 0, -1] 115 | time = mktime(time) 116 | times = (time, time) 117 | for f in args: 118 | if not os.path.exists(f): 119 | open(f, 'a').close() 120 | os.utime(f, times) 121 | -------------------------------------------------------------------------------- /pymake/command.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | """ 5 | Makefile execution. 6 | 7 | Multiple `makes` can be run within the same process. Each one has an entirely data.Makefile and .Target 8 | structure, environment, and working directory. Typically they will all share a parallel execution context, 9 | except when a submake specifies -j1 when the parent make is building in parallel. 10 | """ 11 | 12 | import os, subprocess, sys, logging, time, traceback, re 13 | from optparse import OptionParser 14 | import data, parserdata, process, util 15 | from pymake import errors 16 | 17 | # TODO: If this ever goes from relocatable package to system-installed, this may need to be 18 | # a configured-in path. 19 | 20 | makepypath = util.normaljoin(os.path.dirname(__file__), '../make.py') 21 | 22 | _simpleopts = re.compile(r'^[a-zA-Z]+(\s|$)') 23 | def parsemakeflags(env): 24 | """ 25 | Parse MAKEFLAGS from the environment into a sequence of command-line arguments. 26 | """ 27 | 28 | makeflags = env.get('MAKEFLAGS', '') 29 | makeflags = makeflags.strip() 30 | 31 | if makeflags == '': 32 | return [] 33 | 34 | if _simpleopts.match(makeflags): 35 | makeflags = '-' + makeflags 36 | 37 | opts = [] 38 | curopt = '' 39 | 40 | i = 0 41 | while i < len(makeflags): 42 | c = makeflags[i] 43 | if c.isspace(): 44 | opts.append(curopt) 45 | curopt = '' 46 | i += 1 47 | while i < len(makeflags) and makeflags[i].isspace(): 48 | i += 1 49 | continue 50 | 51 | if c == '\\': 52 | i += 1 53 | if i == len(makeflags): 54 | raise errors.DataError("MAKEFLAGS has trailing backslash") 55 | c = makeflags[i] 56 | 57 | curopt += c 58 | i += 1 59 | 60 | if curopt != '': 61 | opts.append(curopt) 62 | 63 | return opts 64 | 65 | def _version(*args): 66 | print("""pymake: GNU-compatible make program 67 | Copyright (C) 2009 The Mozilla Foundation 68 | This is free software; see the source for copying conditions. 69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 70 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 71 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 72 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 73 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 74 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 75 | DEALINGS IN THE SOFTWARE.""") 76 | 77 | _log = logging.getLogger('pymake.execution') 78 | 79 | class _MakeContext(object): 80 | def __init__(self, makeflags, makelevel, workdir, context, env, targets, options, ostmts, overrides, cb): 81 | self.makeflags = makeflags 82 | self.makelevel = makelevel 83 | 84 | self.workdir = workdir 85 | self.context = context 86 | self.env = env 87 | self.targets = targets 88 | self.options = options 89 | self.ostmts = ostmts 90 | self.overrides = overrides 91 | self.cb = cb 92 | 93 | self.restarts = 0 94 | 95 | self.remakecb(True) 96 | 97 | def remakecb(self, remade, error=None): 98 | if error is not None: 99 | print(error) 100 | self.context.defer(self.cb, 2) 101 | return 102 | 103 | if remade: 104 | if self.restarts > 0: 105 | _log.info("make.py[%i]: Restarting makefile parsing", self.makelevel) 106 | 107 | self.makefile = data.Makefile(restarts=self.restarts, 108 | make='%s %s' % (sys.executable.replace('\\', '/'), makepypath.replace('\\', '/')), 109 | makeflags=self.makeflags, 110 | makeoverrides=self.overrides, 111 | workdir=self.workdir, 112 | context=self.context, 113 | env=self.env, 114 | makelevel=self.makelevel, 115 | targets=self.targets, 116 | keepgoing=self.options.keepgoing, 117 | silent=self.options.silent, 118 | justprint=self.options.justprint) 119 | 120 | self.restarts += 1 121 | 122 | try: 123 | self.ostmts.execute(self.makefile) 124 | for f in self.options.makefiles: 125 | self.makefile.include(f) 126 | self.makefile.finishparsing() 127 | self.makefile.remakemakefiles(self.remakecb) 128 | except errors.MakeError as e: 129 | print(e) 130 | self.context.defer(self.cb, 2) 131 | 132 | return 133 | 134 | if len(self.targets) == 0: 135 | if self.makefile.defaulttarget is None: 136 | print("No target specified and no default target found.") 137 | self.context.defer(self.cb, 2) 138 | return 139 | 140 | _log.info("Making default target %s", self.makefile.defaulttarget) 141 | self.realtargets = [self.makefile.defaulttarget] 142 | self.tstack = [''] 143 | else: 144 | self.realtargets = self.targets 145 | self.tstack = [''] 146 | 147 | self.makefile.gettarget(self.realtargets.pop(0)).make(self.makefile, self.tstack, cb=self.makecb) 148 | 149 | def makecb(self, error, didanything): 150 | assert error in (True, False) 151 | 152 | if error: 153 | self.context.defer(self.cb, 2) 154 | return 155 | 156 | if not len(self.realtargets): 157 | if self.options.printdir: 158 | print("make.py[%i]: Leaving directory '%s'" % (self.makelevel, self.workdir)) 159 | sys.stdout.flush() 160 | 161 | self.context.defer(self.cb, 0) 162 | else: 163 | self.makefile.gettarget(self.realtargets.pop(0)).make(self.makefile, self.tstack, self.makecb) 164 | 165 | def main(args, env, cwd, cb): 166 | """ 167 | Start a single makefile execution, given a command line, working directory, and environment. 168 | 169 | @param cb a callback to notify with an exit code when make execution is finished. 170 | """ 171 | 172 | try: 173 | makelevel = int(env.get('MAKELEVEL', '0')) 174 | 175 | op = OptionParser() 176 | op.add_option('-f', '--file', '--makefile', 177 | action='append', 178 | dest='makefiles', 179 | default=[]) 180 | op.add_option('-d', 181 | action="store_true", 182 | dest="verbose", default=False) 183 | op.add_option('-k', '--keep-going', 184 | action="store_true", 185 | dest="keepgoing", default=False) 186 | op.add_option('--debug-log', 187 | dest="debuglog", default=None) 188 | op.add_option('-C', '--directory', 189 | dest="directory", default=None) 190 | op.add_option('-v', '--version', action="store_true", 191 | dest="printversion", default=False) 192 | op.add_option('-j', '--jobs', type="int", 193 | dest="jobcount", default=1) 194 | op.add_option('-w', '--print-directory', action="store_true", 195 | dest="printdir") 196 | op.add_option('--no-print-directory', action="store_false", 197 | dest="printdir", default=True) 198 | op.add_option('-s', '--silent', action="store_true", 199 | dest="silent", default=False) 200 | op.add_option('-n', '--just-print', '--dry-run', '--recon', 201 | action="store_true", 202 | dest="justprint", default=False) 203 | 204 | options, arguments1 = op.parse_args(parsemakeflags(env)) 205 | options, arguments2 = op.parse_args(args, values=options) 206 | 207 | op.destroy() 208 | 209 | arguments = arguments1 + arguments2 210 | 211 | if options.printversion: 212 | _version() 213 | cb(0) 214 | return 215 | 216 | shortflags = [] 217 | longflags = [] 218 | 219 | if options.keepgoing: 220 | shortflags.append('k') 221 | 222 | if options.printdir: 223 | shortflags.append('w') 224 | 225 | if options.silent: 226 | shortflags.append('s') 227 | options.printdir = False 228 | 229 | if options.justprint: 230 | shortflags.append('n') 231 | 232 | loglevel = logging.WARNING 233 | if options.verbose: 234 | loglevel = logging.DEBUG 235 | shortflags.append('d') 236 | 237 | logkwargs = {} 238 | if options.debuglog: 239 | logkwargs['filename'] = options.debuglog 240 | longflags.append('--debug-log=%s' % options.debuglog) 241 | 242 | if options.directory is None: 243 | workdir = cwd 244 | else: 245 | workdir = util.normaljoin(cwd, options.directory) 246 | 247 | if options.jobcount != 1: 248 | longflags.append('-j%i' % (options.jobcount,)) 249 | 250 | makeflags = ''.join(shortflags) 251 | if len(longflags): 252 | makeflags += ' ' + ' '.join(longflags) 253 | 254 | logging.basicConfig(level=loglevel, **logkwargs) 255 | 256 | context = process.getcontext(options.jobcount) 257 | 258 | if options.printdir: 259 | print("make.py[%i]: Entering directory '%s'" % (makelevel, workdir)) 260 | sys.stdout.flush() 261 | 262 | if len(options.makefiles) == 0: 263 | if os.path.exists(util.normaljoin(workdir, 'Makefile')): 264 | options.makefiles.append('Makefile') 265 | else: 266 | print("No makefile found") 267 | cb(2) 268 | return 269 | 270 | ostmts, targets, overrides = parserdata.parsecommandlineargs(arguments) 271 | 272 | _MakeContext(makeflags, makelevel, workdir, context, env, targets, options, ostmts, overrides, cb) 273 | except errors.MakeError as e: 274 | print(e) 275 | if options.printdir: 276 | print("make.py[%i]: Leaving directory '%s'" % (makelevel, workdir)) 277 | sys.stdout.flush() 278 | cb(2) 279 | return 280 | -------------------------------------------------------------------------------- /pymake/errors.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class MakeError(Exception): 4 | def __init__(self, message, loc=None): 5 | self.msg = message 6 | self.loc = loc 7 | 8 | def __str__(self): 9 | locstr = '' 10 | if self.loc is not None: 11 | locstr = "%s:" % (self.loc,) 12 | 13 | return "%s%s" % (locstr, self.msg) 14 | 15 | 16 | class SyntaxError(MakeError): 17 | pass 18 | 19 | 20 | class DataError(MakeError): 21 | pass 22 | 23 | 24 | class ResolutionError(DataError): 25 | """ 26 | Raised when dependency resolution fails, either due to recursion or to missing 27 | prerequisites.This is separately catchable so that implicit rule search can try things 28 | without having to commit. 29 | """ 30 | pass 31 | 32 | 33 | class PythonError(Exception): 34 | def __init__(self, message, exitcode): 35 | Exception.__init__(self) 36 | self.message = message 37 | self.exitcode = exitcode 38 | 39 | def __str__(self): 40 | return self.message 41 | 42 | 43 | -------------------------------------------------------------------------------- /pymake/functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Makefile functions. 3 | """ 4 | from __future__ import print_function 5 | 6 | import parser, util 7 | import subprocess, os, logging, sys 8 | from globrelative import glob 9 | from pymake import errors 10 | 11 | log = logging.getLogger('pymake.data') 12 | 13 | def emit_expansions(descend, *expansions): 14 | """Helper function to emit all expansions within an input set.""" 15 | for expansion in expansions: 16 | yield expansion 17 | 18 | if not descend or not isinstance(expansion, list): 19 | continue 20 | 21 | for e, is_func in expansion: 22 | if is_func: 23 | for exp in e.expansions(True): 24 | yield exp 25 | else: 26 | yield e 27 | 28 | class Function(object): 29 | """ 30 | An object that represents a function call. This class is always subclassed 31 | with the following methods and attributes: 32 | 33 | minargs = minimum # of arguments 34 | maxargs = maximum # of arguments (0 means unlimited) 35 | 36 | def resolve(self, makefile, variables, fd, setting) 37 | Calls the function 38 | calls fd.write() with strings 39 | """ 40 | 41 | __slots__ = ('_arguments', 'loc') 42 | 43 | def __init__(self, loc): 44 | self._arguments = [] 45 | self.loc = loc 46 | assert self.minargs > 0 47 | 48 | def __getitem__(self, key): 49 | return self._arguments[key] 50 | 51 | def setup(self): 52 | argc = len(self._arguments) 53 | 54 | if argc < self.minargs: 55 | raise errors.DataError("Not enough arguments to function %s, requires %s" % (self.name, self.minargs), self.loc) 56 | 57 | assert self.maxargs == 0 or argc <= self.maxargs, "Parser screwed up, gave us too many args" 58 | 59 | def append(self, arg): 60 | assert isinstance(arg, (data.Expansion, data.StringExpansion)) 61 | self._arguments.append(arg) 62 | 63 | def to_source(self): 64 | """Convert the function back to make file "source" code.""" 65 | if not hasattr(self, 'name'): 66 | raise Exception("%s must implement to_source()." % self.__class__) 67 | 68 | # The default implementation simply prints the function name and all 69 | # the arguments joined by a comma. 70 | # According to the GNU make manual Section 8.1, whitespace around 71 | # arguments is *not* part of the argument's value. So, we trim excess 72 | # white space so we have consistent behavior. 73 | args = [] 74 | curly = False 75 | for i, arg in enumerate(self._arguments): 76 | arg = arg.to_source() 77 | 78 | if i == 0: 79 | arg = arg.lstrip() 80 | 81 | # Are balanced parens even OK? 82 | if arg.count('(') != arg.count(')'): 83 | curly = True 84 | 85 | args.append(arg) 86 | 87 | if curly: 88 | return '${%s %s}' % (self.name, ','.join(args)) 89 | 90 | return '$(%s %s)' % (self.name, ','.join(args)) 91 | 92 | def expansions(self, descend=False): 93 | """Obtain all expansions contained within this function. 94 | 95 | By default, only expansions directly part of this function are 96 | returned. If descend is True, we will descend into child expansions and 97 | return all of the composite parts. 98 | 99 | This is a generator for pymake.data.BaseExpansion instances. 100 | """ 101 | # Our default implementation simply returns arguments. More advanced 102 | # functions like variable references may need their own implementation. 103 | return emit_expansions(descend, *self._arguments) 104 | 105 | @property 106 | def is_filesystem_dependent(self): 107 | """Exposes whether this function depends on the filesystem for results. 108 | 109 | If True, the function touches the filesystem as part of evaluation. 110 | 111 | This only tests whether the function itself uses the filesystem. If 112 | this function has arguments that are functions that touch the 113 | filesystem, this will return False. 114 | """ 115 | return False 116 | 117 | def __len__(self): 118 | return len(self._arguments) 119 | 120 | def __repr__(self): 121 | return "%s<%s>(%r)" % ( 122 | self.__class__.__name__, self.loc, 123 | ','.join([repr(a) for a in self._arguments]), 124 | ) 125 | 126 | def __eq__(self, other): 127 | if not hasattr(self, 'name'): 128 | raise Exception("%s must implement __eq__." % self.__class__) 129 | 130 | if type(self) != type(other): 131 | return False 132 | 133 | if self.name != other.name: 134 | return False 135 | 136 | if len(self._arguments) != len(other._arguments): 137 | return False 138 | 139 | for i in range(len(self._arguments)): 140 | # According to the GNU make manual Section 8.1, whitespace around 141 | # arguments is *not* part of the argument's value. So, we do a 142 | # whitespace-agnostic comparison. 143 | if i == 0: 144 | a = self._arguments[i] 145 | a.lstrip() 146 | 147 | b = other._arguments[i] 148 | b.lstrip() 149 | 150 | if a != b: 151 | return False 152 | 153 | continue 154 | 155 | if self._arguments[i] != other._arguments[i]: 156 | return False 157 | 158 | return True 159 | 160 | def __ne__(self, other): 161 | return not self.__eq__(other) 162 | 163 | class VariableRef(Function): 164 | AUTOMATIC_VARIABLES = set(['@', '%', '<', '?', '^', '+', '|', '*']) 165 | 166 | __slots__ = ('vname', 'loc') 167 | 168 | def __init__(self, loc, vname): 169 | self.loc = loc 170 | assert isinstance(vname, (data.Expansion, data.StringExpansion)) 171 | self.vname = vname 172 | 173 | def setup(self): 174 | assert False, "Shouldn't get here" 175 | 176 | def resolve(self, makefile, variables, fd, setting): 177 | vname = self.vname.resolvestr(makefile, variables, setting) 178 | if vname in setting: 179 | raise errors.DataError("Setting variable '%s' recursively references itself." % (vname,), self.loc) 180 | 181 | flavor, source, value = variables.get(vname) 182 | if value is None: 183 | log.debug("%s: variable '%s' was not set" % (self.loc, vname)) 184 | return 185 | 186 | value.resolve(makefile, variables, fd, setting + [vname]) 187 | 188 | def to_source(self): 189 | if isinstance(self.vname, data.StringExpansion): 190 | if self.vname.s in self.AUTOMATIC_VARIABLES: 191 | return '$%s' % self.vname.s 192 | 193 | return '$(%s)' % self.vname.s 194 | 195 | return '$(%s)' % self.vname.to_source() 196 | 197 | def expansions(self, descend=False): 198 | return emit_expansions(descend, self.vname) 199 | 200 | def __repr__(self): 201 | return "VariableRef<%s>(%r)" % (self.loc, self.vname) 202 | 203 | def __eq__(self, other): 204 | if not isinstance(other, VariableRef): 205 | return False 206 | 207 | return self.vname == other.vname 208 | 209 | class SubstitutionRef(Function): 210 | """$(VARNAME:.c=.o) and $(VARNAME:%.c=%.o)""" 211 | 212 | __slots__ = ('loc', 'vname', 'substfrom', 'substto') 213 | 214 | def __init__(self, loc, varname, substfrom, substto): 215 | self.loc = loc 216 | self.vname = varname 217 | self.substfrom = substfrom 218 | self.substto = substto 219 | 220 | def setup(self): 221 | assert False, "Shouldn't get here" 222 | 223 | def resolve(self, makefile, variables, fd, setting): 224 | vname = self.vname.resolvestr(makefile, variables, setting) 225 | if vname in setting: 226 | raise errors.DataError("Setting variable '%s' recursively references itself." % (vname,), self.loc) 227 | 228 | substfrom = self.substfrom.resolvestr(makefile, variables, setting) 229 | substto = self.substto.resolvestr(makefile, variables, setting) 230 | 231 | flavor, source, value = variables.get(vname) 232 | if value is None: 233 | log.debug("%s: variable '%s' was not set" % (self.loc, vname)) 234 | return 235 | 236 | f = data.Pattern(substfrom) 237 | if not f.ispattern(): 238 | f = data.Pattern('%' + substfrom) 239 | substto = '%' + substto 240 | 241 | fd.write(' '.join([f.subst(substto, word, False) 242 | for word in value.resolvesplit(makefile, variables, setting + [vname])])) 243 | 244 | def to_source(self): 245 | return '$(%s:%s=%s)' % ( 246 | self.vname.to_source(), 247 | self.substfrom.to_source(), 248 | self.substto.to_source()) 249 | 250 | def expansions(self, descend=False): 251 | return emit_expansions(descend, self.vname, self.substfrom, 252 | self.substto) 253 | 254 | def __repr__(self): 255 | return "SubstitutionRef<%s>(%r:%r=%r)" % ( 256 | self.loc, self.vname, self.substfrom, self.substto,) 257 | 258 | def __eq__(self, other): 259 | if not isinstance(other, SubstitutionRef): 260 | return False 261 | 262 | return self.vname == other.vname and self.substfrom == other.substfrom \ 263 | and self.substto == other.substto 264 | 265 | class SubstFunction(Function): 266 | name = 'subst' 267 | minargs = 3 268 | maxargs = 3 269 | 270 | __slots__ = Function.__slots__ 271 | 272 | def resolve(self, makefile, variables, fd, setting): 273 | s = self._arguments[0].resolvestr(makefile, variables, setting) 274 | r = self._arguments[1].resolvestr(makefile, variables, setting) 275 | d = self._arguments[2].resolvestr(makefile, variables, setting) 276 | fd.write(d.replace(s, r)) 277 | 278 | class PatSubstFunction(Function): 279 | name = 'patsubst' 280 | minargs = 3 281 | maxargs = 3 282 | 283 | __slots__ = Function.__slots__ 284 | 285 | def resolve(self, makefile, variables, fd, setting): 286 | s = self._arguments[0].resolvestr(makefile, variables, setting) 287 | r = self._arguments[1].resolvestr(makefile, variables, setting) 288 | 289 | p = data.Pattern(s) 290 | fd.write(' '.join([p.subst(r, word, False) 291 | for word in self._arguments[2].resolvesplit(makefile, variables, setting)])) 292 | 293 | class StripFunction(Function): 294 | name = 'strip' 295 | minargs = 1 296 | maxargs = 1 297 | 298 | __slots__ = Function.__slots__ 299 | 300 | def resolve(self, makefile, variables, fd, setting): 301 | util.joiniter(fd, self._arguments[0].resolvesplit(makefile, variables, setting)) 302 | 303 | class FindstringFunction(Function): 304 | name = 'findstring' 305 | minargs = 2 306 | maxargs = 2 307 | 308 | __slots__ = Function.__slots__ 309 | 310 | def resolve(self, makefile, variables, fd, setting): 311 | s = self._arguments[0].resolvestr(makefile, variables, setting) 312 | r = self._arguments[1].resolvestr(makefile, variables, setting) 313 | if r.find(s) == -1: 314 | return 315 | fd.write(s) 316 | 317 | class FilterFunction(Function): 318 | name = 'filter' 319 | minargs = 2 320 | maxargs = 2 321 | 322 | __slots__ = Function.__slots__ 323 | 324 | def resolve(self, makefile, variables, fd, setting): 325 | plist = [data.Pattern(p) 326 | for p in self._arguments[0].resolvesplit(makefile, variables, setting)] 327 | 328 | fd.write(' '.join([w for w in self._arguments[1].resolvesplit(makefile, variables, setting) 329 | if util.any((p.match(w) for p in plist))])) 330 | 331 | class FilteroutFunction(Function): 332 | name = 'filter-out' 333 | minargs = 2 334 | maxargs = 2 335 | 336 | __slots__ = Function.__slots__ 337 | 338 | def resolve(self, makefile, variables, fd, setting): 339 | plist = [data.Pattern(p) 340 | for p in self._arguments[0].resolvesplit(makefile, variables, setting)] 341 | 342 | fd.write(' '.join([w for w in self._arguments[1].resolvesplit(makefile, variables, setting) 343 | if not util.any((p.match(w) for p in plist))])) 344 | 345 | class SortFunction(Function): 346 | name = 'sort' 347 | minargs = 1 348 | maxargs = 1 349 | 350 | __slots__ = Function.__slots__ 351 | 352 | def resolve(self, makefile, variables, fd, setting): 353 | d = set(self._arguments[0].resolvesplit(makefile, variables, setting)) 354 | util.joiniter(fd, sorted(d)) 355 | 356 | class WordFunction(Function): 357 | name = 'word' 358 | minargs = 2 359 | maxargs = 2 360 | 361 | __slots__ = Function.__slots__ 362 | 363 | def resolve(self, makefile, variables, fd, setting): 364 | n = self._arguments[0].resolvestr(makefile, variables, setting) 365 | # TODO: provide better error if this doesn't convert 366 | n = int(n) 367 | words = list(self._arguments[1].resolvesplit(makefile, variables, setting)) 368 | if n < 1 or n > len(words): 369 | return 370 | fd.write(words[n - 1]) 371 | 372 | class WordlistFunction(Function): 373 | name = 'wordlist' 374 | minargs = 3 375 | maxargs = 3 376 | 377 | __slots__ = Function.__slots__ 378 | 379 | def resolve(self, makefile, variables, fd, setting): 380 | nfrom = self._arguments[0].resolvestr(makefile, variables, setting) 381 | nto = self._arguments[1].resolvestr(makefile, variables, setting) 382 | # TODO: provide better errors if this doesn't convert 383 | nfrom = int(nfrom) 384 | nto = int(nto) 385 | 386 | words = list(self._arguments[2].resolvesplit(makefile, variables, setting)) 387 | 388 | if nfrom < 1: 389 | nfrom = 1 390 | if nto < 1: 391 | nto = 1 392 | 393 | util.joiniter(fd, words[nfrom - 1:nto]) 394 | 395 | class WordsFunction(Function): 396 | name = 'words' 397 | minargs = 1 398 | maxargs = 1 399 | 400 | __slots__ = Function.__slots__ 401 | 402 | def resolve(self, makefile, variables, fd, setting): 403 | fd.write(str(len(self._arguments[0].resolvesplit(makefile, variables, setting)))) 404 | 405 | class FirstWordFunction(Function): 406 | name = 'firstword' 407 | minargs = 1 408 | maxargs = 1 409 | 410 | __slots__ = Function.__slots__ 411 | 412 | def resolve(self, makefile, variables, fd, setting): 413 | l = self._arguments[0].resolvesplit(makefile, variables, setting) 414 | if len(l): 415 | fd.write(l[0]) 416 | 417 | class LastWordFunction(Function): 418 | name = 'lastword' 419 | minargs = 1 420 | maxargs = 1 421 | 422 | __slots__ = Function.__slots__ 423 | 424 | def resolve(self, makefile, variables, fd, setting): 425 | l = self._arguments[0].resolvesplit(makefile, variables, setting) 426 | if len(l): 427 | fd.write(l[-1]) 428 | 429 | def pathsplit(path, default='./'): 430 | """ 431 | Splits a path into dirpart, filepart on the last slash. If there is no slash, dirpart 432 | is ./ 433 | """ 434 | dir, slash, file = util.strrpartition(path, '/') 435 | if dir == '': 436 | return default, file 437 | 438 | return dir + slash, file 439 | 440 | class DirFunction(Function): 441 | name = 'dir' 442 | minargs = 1 443 | maxargs = 1 444 | 445 | def resolve(self, makefile, variables, fd, setting): 446 | fd.write(' '.join([pathsplit(path)[0] 447 | for path in self._arguments[0].resolvesplit(makefile, variables, setting)])) 448 | 449 | class NotDirFunction(Function): 450 | name = 'notdir' 451 | minargs = 1 452 | maxargs = 1 453 | 454 | __slots__ = Function.__slots__ 455 | 456 | def resolve(self, makefile, variables, fd, setting): 457 | fd.write(' '.join([pathsplit(path)[1] 458 | for path in self._arguments[0].resolvesplit(makefile, variables, setting)])) 459 | 460 | class SuffixFunction(Function): 461 | name = 'suffix' 462 | minargs = 1 463 | maxargs = 1 464 | 465 | __slots__ = Function.__slots__ 466 | 467 | @staticmethod 468 | def suffixes(words): 469 | for w in words: 470 | dir, file = pathsplit(w) 471 | base, dot, suffix = util.strrpartition(file, '.') 472 | if base != '': 473 | yield dot + suffix 474 | 475 | def resolve(self, makefile, variables, fd, setting): 476 | util.joiniter(fd, self.suffixes(self._arguments[0].resolvesplit(makefile, variables, setting))) 477 | 478 | class BasenameFunction(Function): 479 | name = 'basename' 480 | minargs = 1 481 | maxargs = 1 482 | 483 | __slots__ = Function.__slots__ 484 | 485 | @staticmethod 486 | def basenames(words): 487 | for w in words: 488 | dir, file = pathsplit(w, '') 489 | base, dot, suffix = util.strrpartition(file, '.') 490 | if dot == '': 491 | base = suffix 492 | 493 | yield dir + base 494 | 495 | def resolve(self, makefile, variables, fd, setting): 496 | util.joiniter(fd, self.basenames(self._arguments[0].resolvesplit(makefile, variables, setting))) 497 | 498 | class AddSuffixFunction(Function): 499 | name = 'addsuffix' 500 | minargs = 2 501 | maxargs = 2 502 | 503 | __slots__ = Function.__slots__ 504 | 505 | def resolve(self, makefile, variables, fd, setting): 506 | suffix = self._arguments[0].resolvestr(makefile, variables, setting) 507 | 508 | fd.write(' '.join([w + suffix for w in self._arguments[1].resolvesplit(makefile, variables, setting)])) 509 | 510 | class AddPrefixFunction(Function): 511 | name = 'addprefix' 512 | minargs = 2 513 | maxargs = 2 514 | 515 | def resolve(self, makefile, variables, fd, setting): 516 | prefix = self._arguments[0].resolvestr(makefile, variables, setting) 517 | 518 | fd.write(' '.join([prefix + w for w in self._arguments[1].resolvesplit(makefile, variables, setting)])) 519 | 520 | class JoinFunction(Function): 521 | name = 'join' 522 | minargs = 2 523 | maxargs = 2 524 | 525 | __slots__ = Function.__slots__ 526 | 527 | @staticmethod 528 | def iterjoin(l1, l2): 529 | for i in range(0, max(len(l1), len(l2))): 530 | i1 = i < len(l1) and l1[i] or '' 531 | i2 = i < len(l2) and l2[i] or '' 532 | yield i1 + i2 533 | 534 | def resolve(self, makefile, variables, fd, setting): 535 | list1 = list(self._arguments[0].resolvesplit(makefile, variables, setting)) 536 | list2 = list(self._arguments[1].resolvesplit(makefile, variables, setting)) 537 | 538 | util.joiniter(fd, self.iterjoin(list1, list2)) 539 | 540 | class WildcardFunction(Function): 541 | name = 'wildcard' 542 | minargs = 1 543 | maxargs = 1 544 | 545 | __slots__ = Function.__slots__ 546 | 547 | def resolve(self, makefile, variables, fd, setting): 548 | patterns = self._arguments[0].resolvesplit(makefile, variables, setting) 549 | 550 | fd.write(' '.join([x.replace('\\','/') 551 | for p in patterns 552 | for x in glob(makefile.workdir, p)])) 553 | 554 | @property 555 | def is_filesystem_dependent(self): 556 | return True 557 | 558 | class RealpathFunction(Function): 559 | name = 'realpath' 560 | minargs = 1 561 | maxargs = 1 562 | 563 | def resolve(self, makefile, variables, fd, setting): 564 | fd.write(' '.join([os.path.realpath(os.path.join(makefile.workdir, path)).replace('\\', '/') 565 | for path in self._arguments[0].resolvesplit(makefile, variables, setting)])) 566 | 567 | def is_filesystem_dependent(self): 568 | return True 569 | 570 | class AbspathFunction(Function): 571 | name = 'abspath' 572 | minargs = 1 573 | maxargs = 1 574 | 575 | __slots__ = Function.__slots__ 576 | 577 | def resolve(self, makefile, variables, fd, setting): 578 | assert os.path.isabs(makefile.workdir) 579 | fd.write(' '.join([util.normaljoin(makefile.workdir, path).replace('\\', '/') 580 | for path in self._arguments[0].resolvesplit(makefile, variables, setting)])) 581 | 582 | class IfFunction(Function): 583 | name = 'if' 584 | minargs = 1 585 | maxargs = 3 586 | 587 | __slots__ = Function.__slots__ 588 | 589 | def setup(self): 590 | Function.setup(self) 591 | self._arguments[0].lstrip() 592 | self._arguments[0].rstrip() 593 | 594 | def resolve(self, makefile, variables, fd, setting): 595 | condition = self._arguments[0].resolvestr(makefile, variables, setting) 596 | 597 | if len(condition): 598 | self._arguments[1].resolve(makefile, variables, fd, setting) 599 | elif len(self._arguments) > 2: 600 | return self._arguments[2].resolve(makefile, variables, fd, setting) 601 | 602 | class OrFunction(Function): 603 | name = 'or' 604 | minargs = 1 605 | maxargs = 0 606 | 607 | __slots__ = Function.__slots__ 608 | 609 | def resolve(self, makefile, variables, fd, setting): 610 | for arg in self._arguments: 611 | r = arg.resolvestr(makefile, variables, setting) 612 | if r != '': 613 | fd.write(r) 614 | return 615 | 616 | class AndFunction(Function): 617 | name = 'and' 618 | minargs = 1 619 | maxargs = 0 620 | 621 | __slots__ = Function.__slots__ 622 | 623 | def resolve(self, makefile, variables, fd, setting): 624 | r = '' 625 | 626 | for arg in self._arguments: 627 | r = arg.resolvestr(makefile, variables, setting) 628 | if r == '': 629 | return 630 | 631 | fd.write(r) 632 | 633 | class ForEachFunction(Function): 634 | name = 'foreach' 635 | minargs = 3 636 | maxargs = 3 637 | 638 | __slots__ = Function.__slots__ 639 | 640 | def resolve(self, makefile, variables, fd, setting): 641 | vname = self._arguments[0].resolvestr(makefile, variables, setting) 642 | e = self._arguments[2] 643 | 644 | v = data.Variables(parent=variables) 645 | firstword = True 646 | 647 | for w in self._arguments[1].resolvesplit(makefile, variables, setting): 648 | if firstword: 649 | firstword = False 650 | else: 651 | fd.write(' ') 652 | 653 | # The $(origin) of the local variable must be "automatic" to 654 | # conform with GNU make. However, automatic variables have low 655 | # priority. So, we must force its assignment to occur. 656 | v.set(vname, data.Variables.FLAVOR_SIMPLE, 657 | data.Variables.SOURCE_AUTOMATIC, w, force=True) 658 | e.resolve(makefile, v, fd, setting) 659 | 660 | class CallFunction(Function): 661 | name = 'call' 662 | minargs = 1 663 | maxargs = 0 664 | 665 | __slots__ = Function.__slots__ 666 | 667 | def resolve(self, makefile, variables, fd, setting): 668 | vname = self._arguments[0].resolvestr(makefile, variables, setting) 669 | if vname in setting: 670 | raise errors.DataError("Recursively setting variable '%s'" % (vname,)) 671 | 672 | v = data.Variables(parent=variables) 673 | v.set('0', data.Variables.FLAVOR_SIMPLE, data.Variables.SOURCE_AUTOMATIC, vname) 674 | for i in range(1, len(self._arguments)): 675 | param = self._arguments[i].resolvestr(makefile, variables, setting) 676 | v.set(str(i), data.Variables.FLAVOR_SIMPLE, data.Variables.SOURCE_AUTOMATIC, param) 677 | 678 | flavor, source, e = variables.get(vname) 679 | 680 | if e is None: 681 | return 682 | 683 | if flavor == data.Variables.FLAVOR_SIMPLE: 684 | log.warning("%s: calling variable '%s' which is simply-expanded" % (self.loc, vname)) 685 | 686 | # but we'll do it anyway 687 | e.resolve(makefile, v, fd, setting + [vname]) 688 | 689 | class ValueFunction(Function): 690 | name = 'value' 691 | minargs = 1 692 | maxargs = 1 693 | 694 | __slots__ = Function.__slots__ 695 | 696 | def resolve(self, makefile, variables, fd, setting): 697 | varname = self._arguments[0].resolvestr(makefile, variables, setting) 698 | 699 | flavor, source, value = variables.get(varname, expand=False) 700 | if value is not None: 701 | fd.write(value) 702 | 703 | class EvalFunction(Function): 704 | name = 'eval' 705 | minargs = 1 706 | maxargs = 1 707 | 708 | def resolve(self, makefile, variables, fd, setting): 709 | if makefile.parsingfinished: 710 | # GNU make allows variables to be set by recursive expansion during 711 | # command execution. This seems really dumb to me, so I don't! 712 | raise errors.DataError("$(eval) not allowed via recursive expansion after parsing is finished", self.loc) 713 | 714 | stmts = parser.parsestring(self._arguments[0].resolvestr(makefile, variables, setting), 715 | 'evaluation from %s' % self.loc) 716 | stmts.execute(makefile) 717 | 718 | class OriginFunction(Function): 719 | name = 'origin' 720 | minargs = 1 721 | maxargs = 1 722 | 723 | __slots__ = Function.__slots__ 724 | 725 | def resolve(self, makefile, variables, fd, setting): 726 | vname = self._arguments[0].resolvestr(makefile, variables, setting) 727 | 728 | flavor, source, value = variables.get(vname) 729 | if source is None: 730 | r = 'undefined' 731 | elif source == data.Variables.SOURCE_OVERRIDE: 732 | r = 'override' 733 | 734 | elif source == data.Variables.SOURCE_MAKEFILE: 735 | r = 'file' 736 | elif source == data.Variables.SOURCE_ENVIRONMENT: 737 | r = 'environment' 738 | elif source == data.Variables.SOURCE_COMMANDLINE: 739 | r = 'command line' 740 | elif source == data.Variables.SOURCE_AUTOMATIC: 741 | r = 'automatic' 742 | elif source == data.Variables.SOURCE_IMPLICIT: 743 | r = 'default' 744 | 745 | fd.write(r) 746 | 747 | class FlavorFunction(Function): 748 | name = 'flavor' 749 | minargs = 1 750 | maxargs = 1 751 | 752 | __slots__ = Function.__slots__ 753 | 754 | def resolve(self, makefile, variables, fd, setting): 755 | varname = self._arguments[0].resolvestr(makefile, variables, setting) 756 | 757 | flavor, source, value = variables.get(varname) 758 | if flavor is None: 759 | r = 'undefined' 760 | elif flavor == data.Variables.FLAVOR_RECURSIVE: 761 | r = 'recursive' 762 | elif flavor == data.Variables.FLAVOR_SIMPLE: 763 | r = 'simple' 764 | fd.write(r) 765 | 766 | class ShellFunction(Function): 767 | name = 'shell' 768 | minargs = 1 769 | maxargs = 1 770 | 771 | __slots__ = Function.__slots__ 772 | 773 | def resolve(self, makefile, variables, fd, setting): 774 | from process import prepare_command 775 | cline = self._arguments[0].resolvestr(makefile, variables, setting) 776 | executable, cline = prepare_command(cline, makefile.workdir, self.loc) 777 | 778 | # subprocess.Popen doesn't use the PATH set in the env argument for 779 | # finding the executable on some platforms (but strangely it does on 780 | # others!), so set os.environ['PATH'] explicitly. 781 | oldpath = os.environ['PATH'] 782 | if makefile.env is not None and 'PATH' in makefile.env: 783 | os.environ['PATH'] = makefile.env['PATH'] 784 | 785 | log.debug("%s: running command '%s'" % (self.loc, ' '.join(cline))) 786 | try: 787 | p = subprocess.Popen(cline, executable=executable, env=makefile.env, shell=False, 788 | stdout=subprocess.PIPE, cwd=makefile.workdir) 789 | except OSError as e: 790 | print("Error executing command %s" % cline[0], e, file=sys.stderr) 791 | return 792 | finally: 793 | os.environ['PATH'] = oldpath 794 | 795 | stdout, stderr = p.communicate() 796 | stdout = stdout.replace('\r\n', '\n') 797 | if stdout.endswith('\n'): 798 | stdout = stdout[:-1] 799 | stdout = stdout.replace('\n', ' ') 800 | 801 | fd.write(stdout) 802 | 803 | class ErrorFunction(Function): 804 | name = 'error' 805 | minargs = 1 806 | maxargs = 1 807 | 808 | __slots__ = Function.__slots__ 809 | 810 | def resolve(self, makefile, variables, fd, setting): 811 | v = self._arguments[0].resolvestr(makefile, variables, setting) 812 | raise errors.DataError(v, self.loc) 813 | 814 | class WarningFunction(Function): 815 | name = 'warning' 816 | minargs = 1 817 | maxargs = 1 818 | 819 | __slots__ = Function.__slots__ 820 | 821 | def resolve(self, makefile, variables, fd, setting): 822 | v = self._arguments[0].resolvestr(makefile, variables, setting) 823 | log.warning(v) 824 | 825 | class InfoFunction(Function): 826 | name = 'info' 827 | minargs = 1 828 | maxargs = 1 829 | 830 | __slots__ = Function.__slots__ 831 | 832 | def resolve(self, makefile, variables, fd, setting): 833 | v = self._arguments[0].resolvestr(makefile, variables, setting) 834 | print(v) 835 | 836 | functionmap = { 837 | 'subst': SubstFunction, 838 | 'patsubst': PatSubstFunction, 839 | 'strip': StripFunction, 840 | 'findstring': FindstringFunction, 841 | 'filter': FilterFunction, 842 | 'filter-out': FilteroutFunction, 843 | 'sort': SortFunction, 844 | 'word': WordFunction, 845 | 'wordlist': WordlistFunction, 846 | 'words': WordsFunction, 847 | 'firstword': FirstWordFunction, 848 | 'lastword': LastWordFunction, 849 | 'dir': DirFunction, 850 | 'notdir': NotDirFunction, 851 | 'suffix': SuffixFunction, 852 | 'basename': BasenameFunction, 853 | 'addsuffix': AddSuffixFunction, 854 | 'addprefix': AddPrefixFunction, 855 | 'join': JoinFunction, 856 | 'wildcard': WildcardFunction, 857 | 'realpath': RealpathFunction, 858 | 'abspath': AbspathFunction, 859 | 'if': IfFunction, 860 | 'or': OrFunction, 861 | 'and': AndFunction, 862 | 'foreach': ForEachFunction, 863 | 'call': CallFunction, 864 | 'value': ValueFunction, 865 | 'eval': EvalFunction, 866 | 'origin': OriginFunction, 867 | 'flavor': FlavorFunction, 868 | 'shell': ShellFunction, 869 | 'error': ErrorFunction, 870 | 'warning': WarningFunction, 871 | 'info': InfoFunction, 872 | } 873 | 874 | import data 875 | -------------------------------------------------------------------------------- /pymake/globrelative.py: -------------------------------------------------------------------------------- 1 | """ 2 | Filename globbing like the python glob module with minor differences: 3 | 4 | * glob relative to an arbitrary directory 5 | * include . and .. 6 | * check that link targets exist, not just links 7 | """ 8 | 9 | import os, re, fnmatch 10 | import util 11 | 12 | _globcheck = re.compile('[[*?]') 13 | 14 | def hasglob(p): 15 | return _globcheck.search(p) is not None 16 | 17 | def glob(fsdir, path): 18 | """ 19 | Yield paths matching the path glob. Sorts as a bonus. Excludes '.' and '..' 20 | """ 21 | 22 | dir, leaf = os.path.split(path) 23 | if dir == '': 24 | return globpattern(fsdir, leaf) 25 | 26 | if hasglob(dir): 27 | dirsfound = glob(fsdir, dir) 28 | else: 29 | dirsfound = [dir] 30 | 31 | r = [] 32 | 33 | for dir in dirsfound: 34 | fspath = util.normaljoin(fsdir, dir) 35 | if not os.path.isdir(fspath): 36 | continue 37 | 38 | r.extend((util.normaljoin(dir, found) for found in globpattern(fspath, leaf))) 39 | 40 | return r 41 | 42 | def globpattern(dir, pattern): 43 | """ 44 | Return leaf names in the specified directory which match the pattern. 45 | """ 46 | 47 | if not hasglob(pattern): 48 | if pattern == '': 49 | if os.path.isdir(dir): 50 | return [''] 51 | return [] 52 | 53 | if os.path.exists(util.normaljoin(dir, pattern)): 54 | return [pattern] 55 | return [] 56 | 57 | leaves = os.listdir(dir) + ['.', '..'] 58 | 59 | # "hidden" filenames are a bit special 60 | if not pattern.startswith('.'): 61 | leaves = [leaf for leaf in leaves 62 | if not leaf.startswith('.')] 63 | 64 | leaves = fnmatch.filter(leaves, pattern) 65 | leaves = [l for l in leaves if os.path.exists(util.normaljoin(dir, l))] 66 | 67 | leaves.sort() 68 | return leaves 69 | -------------------------------------------------------------------------------- /pymake/implicit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implicit variables; perhaps in the future this will also include some implicit 3 | rules, at least match-anything cancellation rules. 4 | """ 5 | 6 | variables = { 7 | 'MKDIR': '%pymake.builtins mkdir', 8 | 'RM': '%pymake.builtins rm -f', 9 | 'SLEEP': '%pymake.builtins sleep', 10 | 'TOUCH': '%pymake.builtins touch', 11 | '.LIBPATTERNS': 'lib%.so lib%.a', 12 | '.PYMAKE': '1', 13 | } 14 | 15 | -------------------------------------------------------------------------------- /pymake/process.py: -------------------------------------------------------------------------------- 1 | """ 2 | Skipping shell invocations is good, when possible. This wrapper around subprocess does dirty work of 3 | parsing command lines into argv and making sure that no shell magic is being used. 4 | """ 5 | from __future__ import print_function 6 | 7 | #TODO: ship pyprocessing? 8 | import multiprocessing 9 | import subprocess, shlex, re, logging, sys, traceback, os, imp, glob 10 | import site 11 | from collections import deque 12 | # XXXkhuey Work around http://bugs.python.org/issue1731717 13 | subprocess._cleanup = lambda: None 14 | import command, util 15 | from pymake import errors 16 | if sys.platform=='win32': 17 | import win32process 18 | 19 | _log = logging.getLogger('pymake.process') 20 | 21 | _escapednewlines = re.compile(r'\\\n') 22 | 23 | def tokens2re(tokens): 24 | # Create a pattern for non-escaped tokens, in the form: 25 | # (?pattern) 30 | # which matches the pattern and captures it in a named match group. 31 | # The group names and patterns come are given as a dict in the function 32 | # argument. 33 | nonescaped = r'(?%s)' % (name, value) for name, value in tokens.items()) 34 | # The final pattern matches either the above pattern, or an escaped 35 | # backslash, captured in the "escape" match group. 36 | return re.compile('(?:%s|%s)' % (nonescaped, r'(?P\\\\)')) 37 | 38 | _unquoted_tokens = tokens2re({ 39 | 'whitespace': r'[\t\r\n ]+', 40 | 'quote': r'[\'"]', 41 | 'comment': '#', 42 | 'special': r'[<>&|`~(){}$;]', 43 | 'backslashed': r'\\[^\\]', 44 | 'glob': r'[\*\?]', 45 | }) 46 | 47 | _doubly_quoted_tokens = tokens2re({ 48 | 'quote': '"', 49 | 'backslashedquote': r'\\"', 50 | 'special': '\$', 51 | 'backslashed': r'\\[^\\"]', 52 | }) 53 | 54 | class MetaCharacterException(Exception): 55 | def __init__(self, char): 56 | self.char = char 57 | 58 | class ClineSplitter(list): 59 | """ 60 | Parses a given command line string and creates a list of command 61 | and arguments, with wildcard expansion. 62 | """ 63 | def __init__(self, cline, cwd): 64 | self.cwd = cwd 65 | self.arg = None 66 | self.cline = cline 67 | self.glob = False 68 | self._parse_unquoted() 69 | 70 | def _push(self, str): 71 | """ 72 | Push the given string as part of the current argument 73 | """ 74 | if self.arg is None: 75 | self.arg = '' 76 | self.arg += str 77 | 78 | def _next(self): 79 | """ 80 | Finalize current argument, effectively adding it to the list. 81 | Perform globbing if needed. 82 | """ 83 | if self.arg is None: 84 | return 85 | if self.glob: 86 | if os.path.isabs(self.arg): 87 | path = self.arg 88 | else: 89 | path = os.path.join(self.cwd, self.arg) 90 | globbed = glob.glob(path) 91 | if not globbed: 92 | # If globbing doesn't find anything, the literal string is 93 | # used. 94 | self.append(self.arg) 95 | else: 96 | self.extend(f[len(path)-len(self.arg):] for f in globbed) 97 | self.glob = False 98 | else: 99 | self.append(self.arg) 100 | self.arg = None 101 | 102 | def _parse_unquoted(self): 103 | """ 104 | Parse command line remainder in the context of an unquoted string. 105 | """ 106 | while self.cline: 107 | # Find the next token 108 | m = _unquoted_tokens.search(self.cline) 109 | # If we find none, the remainder of the string can be pushed to 110 | # the current argument and the argument finalized 111 | if not m: 112 | self._push(self.cline) 113 | break 114 | # The beginning of the string, up to the found token, is part of 115 | # the current argument 116 | if m.start(): 117 | self._push(self.cline[:m.start()]) 118 | self.cline = self.cline[m.end():] 119 | 120 | match = dict([(name, value) for name, value in m.groupdict().items() if value]) 121 | if 'quote' in match: 122 | # " or ' start a quoted string 123 | if match['quote'] == '"': 124 | self._parse_doubly_quoted() 125 | else: 126 | self._parse_quoted() 127 | elif 'comment' in match: 128 | # Comments are ignored. The current argument can be finalized, 129 | # and parsing stopped. 130 | break 131 | elif 'special' in match: 132 | # Unquoted, non-escaped special characters need to be sent to a 133 | # shell. 134 | raise MetaCharacterException(match['special']) 135 | elif 'whitespace' in match: 136 | # Whitespaces terminate current argument. 137 | self._next() 138 | elif 'escape' in match: 139 | # Escaped backslashes turn into a single backslash 140 | self._push('\\') 141 | elif 'backslashed' in match: 142 | # Backslashed characters are unbackslashed 143 | # e.g. echo \a -> a 144 | self._push(match['backslashed'][1]) 145 | elif 'glob' in match: 146 | # ? or * will need globbing 147 | self.glob = True 148 | self._push(m.group(0)) 149 | else: 150 | raise Exception("Shouldn't reach here") 151 | if self.arg: 152 | self._next() 153 | 154 | def _parse_quoted(self): 155 | # Single quoted strings are preserved, except for the final quote 156 | index = self.cline.find("'") 157 | if index == -1: 158 | raise Exception('Unterminated quoted string in command') 159 | self._push(self.cline[:index]) 160 | self.cline = self.cline[index+1:] 161 | 162 | def _parse_doubly_quoted(self): 163 | if not self.cline: 164 | raise Exception('Unterminated quoted string in command') 165 | while self.cline: 166 | m = _doubly_quoted_tokens.search(self.cline) 167 | if not m: 168 | raise Exception('Unterminated quoted string in command') 169 | self._push(self.cline[:m.start()]) 170 | self.cline = self.cline[m.end():] 171 | match = dict([(name, value) for name, value in m.groupdict().items() if value]) 172 | if 'quote' in match: 173 | # a double quote ends the quoted string, so go back to 174 | # unquoted parsing 175 | return 176 | elif 'special' in match: 177 | # Unquoted, non-escaped special characters in a doubly quoted 178 | # string still have a special meaning and need to be sent to a 179 | # shell. 180 | raise MetaCharacterException(match['special']) 181 | elif 'escape' in match: 182 | # Escaped backslashes turn into a single backslash 183 | self._push('\\') 184 | elif 'backslashedquote' in match: 185 | # Backslashed double quotes are un-backslashed 186 | self._push('"') 187 | elif 'backslashed' in match: 188 | # Backslashed characters are kept backslashed 189 | self._push(match['backslashed']) 190 | 191 | def clinetoargv(cline, cwd): 192 | """ 193 | If this command line can safely skip the shell, return an argv array. 194 | @returns argv, badchar 195 | """ 196 | str = _escapednewlines.sub('', cline) 197 | try: 198 | args = ClineSplitter(str, cwd) 199 | except MetaCharacterException as e: 200 | return None, e.char 201 | 202 | if len(args) and args[0].find('=') != -1: 203 | return None, '=' 204 | 205 | return args, None 206 | 207 | # shellwords contains a set of shell builtin commands that need to be 208 | # executed within a shell. It also contains a set of commands that are known 209 | # to be giving problems when run directly instead of through the msys shell. 210 | shellwords = (':', '.', 'break', 'cd', 'continue', 'exec', 'exit', 'export', 211 | 'getopts', 'hash', 'pwd', 'readonly', 'return', 'shift', 212 | 'test', 'times', 'trap', 'umask', 'unset', 'alias', 213 | 'set', 'bind', 'builtin', 'caller', 'command', 'declare', 214 | 'echo', 'enable', 'help', 'let', 'local', 'logout', 215 | 'printf', 'read', 'shopt', 'source', 'type', 'typeset', 216 | 'ulimit', 'unalias', 'set', 'find') 217 | 218 | def prepare_command(cline, cwd, loc): 219 | """ 220 | Returns a list of command and arguments for the given command line string. 221 | If the command needs to be run through a shell for some reason, the 222 | returned list contains the shell invocation. 223 | """ 224 | 225 | #TODO: call this once up-front somewhere and save the result? 226 | shell, msys = util.checkmsyscompat() 227 | 228 | shellreason = None 229 | executable = None 230 | if msys and cline.startswith('/'): 231 | shellreason = "command starts with /" 232 | else: 233 | argv, badchar = clinetoargv(cline, cwd) 234 | if argv is None: 235 | shellreason = "command contains shell-special character '%s'" % (badchar,) 236 | elif len(argv) and argv[0] in shellwords: 237 | shellreason = "command starts with shell primitive '%s'" % (argv[0],) 238 | elif argv and (os.sep in argv[0] or os.altsep and os.altsep in argv[0]): 239 | executable = util.normaljoin(cwd, argv[0]) 240 | # Avoid "%1 is not a valid Win32 application" errors, assuming 241 | # that if the executable path is to be resolved with PATH, it will 242 | # be a Win32 executable. 243 | if sys.platform == 'win32' and os.path.isfile(executable) and open(executable, 'rb').read(2) == "#!": 244 | shellreason = "command executable starts with a hashbang" 245 | 246 | if shellreason is not None: 247 | _log.debug("%s: using shell: %s: '%s'", loc, shellreason, cline) 248 | if msys: 249 | if len(cline) > 3 and cline[1] == ':' and cline[2] == '/': 250 | cline = '/' + cline[0] + cline[2:] 251 | argv = [shell, "-c", cline] 252 | executable = None 253 | 254 | return executable, argv 255 | 256 | def call(cline, env, cwd, loc, cb, context, echo, justprint=False): 257 | executable, argv = prepare_command(cline, cwd, loc) 258 | 259 | if not len(argv): 260 | cb(res=0) 261 | return 262 | 263 | if argv[0] == command.makepypath: 264 | command.main(argv[1:], env, cwd, cb) 265 | return 266 | 267 | if argv[0:2] == [sys.executable.replace('\\', '/'), 268 | command.makepypath.replace('\\', '/')]: 269 | command.main(argv[2:], env, cwd, cb) 270 | return 271 | 272 | context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb, 273 | echo=echo, justprint=justprint) 274 | 275 | def call_native(module, method, argv, env, cwd, loc, cb, context, echo, justprint=False, 276 | pycommandpath=None): 277 | context.call_native(module, method, argv, env=env, cwd=cwd, cb=cb, 278 | echo=echo, justprint=justprint, pycommandpath=pycommandpath) 279 | 280 | def statustoresult(status): 281 | """ 282 | Convert the status returned from waitpid into a prettier numeric result. 283 | """ 284 | sig = status & 0xFF 285 | if sig: 286 | return -sig 287 | 288 | return status >>8 289 | 290 | class Job(object): 291 | """ 292 | A single job to be executed on the process pool. 293 | """ 294 | done = False # set to true when the job completes 295 | 296 | def __init__(self): 297 | self.exitcode = -127 298 | 299 | def notify(self, condition, result): 300 | condition.acquire() 301 | self.done = True 302 | self.exitcode = result 303 | condition.notify() 304 | condition.release() 305 | 306 | def get_callback(self, condition): 307 | return lambda result: self.notify(condition, result) 308 | 309 | class PopenJob(Job): 310 | """ 311 | A job that executes a command using subprocess.Popen. 312 | """ 313 | def __init__(self, argv, executable, shell, env, cwd): 314 | Job.__init__(self) 315 | self.argv = argv 316 | self.executable = executable 317 | self.shell = shell 318 | self.env = env 319 | self.cwd = cwd 320 | self.parentpid = os.getpid() 321 | 322 | def run(self): 323 | assert os.getpid() != self.parentpid 324 | # subprocess.Popen doesn't use the PATH set in the env argument for 325 | # finding the executable on some platforms (but strangely it does on 326 | # others!), so set os.environ['PATH'] explicitly. This is parallel- 327 | # safe because pymake uses separate processes for parallelism, and 328 | # each process is serial. See http://bugs.python.org/issue8557 for a 329 | # general overview of "subprocess PATH semantics and portability". 330 | oldpath = os.environ['PATH'] 331 | try: 332 | if self.env is not None and 'PATH' in self.env: 333 | os.environ['PATH'] = self.env['PATH'] 334 | p = subprocess.Popen(self.argv, executable=self.executable, shell=self.shell, env=self.env, cwd=self.cwd) 335 | return p.wait() 336 | except OSError as e: 337 | print(e, file=sys.stderr) 338 | return -127 339 | finally: 340 | os.environ['PATH'] = oldpath 341 | 342 | class PythonJob(Job): 343 | """ 344 | A job that calls a Python method. 345 | """ 346 | def __init__(self, module, method, argv, env, cwd, pycommandpath=None): 347 | self.module = module 348 | self.method = method 349 | self.argv = argv 350 | self.env = env 351 | self.cwd = cwd 352 | self.pycommandpath = pycommandpath or [] 353 | self.parentpid = os.getpid() 354 | 355 | def run(self): 356 | assert os.getpid() != self.parentpid 357 | # os.environ is a magic dictionary. Setting it to something else 358 | # doesn't affect the environment of subprocesses, so use clear/update 359 | oldenv = dict(os.environ) 360 | 361 | # sys.path is adjusted for the entire lifetime of the command 362 | # execution. This ensures any delayed imports will still work. 363 | oldsyspath = list(sys.path) 364 | try: 365 | os.chdir(self.cwd) 366 | os.environ.clear() 367 | os.environ.update(self.env) 368 | 369 | sys.path = [] 370 | for p in sys.path + self.pycommandpath: 371 | site.addsitedir(p) 372 | sys.path.extend(oldsyspath) 373 | 374 | if self.module not in sys.modules: 375 | try: 376 | __import__(self.module) 377 | except Exception as e: 378 | print('Error importing %s: %s' % ( 379 | self.module, e), file=sys.stderr) 380 | return -127 381 | 382 | m = sys.modules[self.module] 383 | if self.method not in m.__dict__: 384 | print("No method named '%s' in module %s" % (self.method, self.module), file=sys.stderr) 385 | return -127 386 | rv = m.__dict__[self.method](self.argv) 387 | if rv != 0 and rv is not None: 388 | print(( 389 | "Native command '%s %s' returned value '%s'" % 390 | (self.module, self.method, rv)), file=sys.stderr) 391 | return (rv if isinstance(rv, int) else 1) 392 | 393 | except errors.PythonError as e: 394 | print(e, file=sys.stderr) 395 | return e.exitcode 396 | except: 397 | e = sys.exc_info()[1] 398 | if isinstance(e, SystemExit) and (e.code == 0 or e.code is None): 399 | pass # sys.exit(0) is not a failure 400 | else: 401 | print(e, file=sys.stderr) 402 | traceback.print_exc() 403 | return -127 404 | finally: 405 | os.environ.clear() 406 | os.environ.update(oldenv) 407 | sys.path = oldsyspath 408 | # multiprocessing exits via os._exit, make sure that all output 409 | # from command gets written out before that happens. 410 | sys.stdout.flush() 411 | sys.stderr.flush() 412 | 413 | return 0 414 | 415 | def job_runner(job): 416 | """ 417 | Run a job. Called in a Process pool. 418 | """ 419 | return job.run() 420 | 421 | class ParallelContext(object): 422 | """ 423 | Manages the parallel execution of processes. 424 | """ 425 | 426 | _allcontexts = set() 427 | _condition = multiprocessing.Condition() 428 | 429 | def __init__(self, jcount): 430 | self.jcount = jcount 431 | self.exit = False 432 | 433 | self.processpool = multiprocessing.Pool(processes=jcount) 434 | self.pending = deque() # deque of (cb, args, kwargs) 435 | self.running = [] # list of (subprocess, cb) 436 | 437 | self._allcontexts.add(self) 438 | 439 | def finish(self): 440 | assert len(self.pending) == 0 and len(self.running) == 0, "pending: %i running: %i" % (len(self.pending), len(self.running)) 441 | self.processpool.close() 442 | self.processpool.join() 443 | self._allcontexts.remove(self) 444 | 445 | def run(self): 446 | while len(self.pending) and len(self.running) < self.jcount: 447 | cb, args, kwargs = self.pending.popleft() 448 | cb(*args, **kwargs) 449 | 450 | def defer(self, cb, *args, **kwargs): 451 | assert self.jcount > 1 or not len(self.pending), "Serial execution error defering %r %r %r: currently pending %r" % (cb, args, kwargs, self.pending) 452 | self.pending.append((cb, args, kwargs)) 453 | 454 | def _docall_generic(self, pool, job, cb, echo, justprint): 455 | if echo is not None: 456 | print(echo) 457 | processcb = job.get_callback(ParallelContext._condition) 458 | if justprint: 459 | processcb(0) 460 | else: 461 | pool.apply_async(job_runner, args=(job,), callback=processcb) 462 | self.running.append((job, cb)) 463 | 464 | def call(self, argv, shell, env, cwd, cb, echo, justprint=False, executable=None): 465 | """ 466 | Asynchronously call the process 467 | """ 468 | 469 | job = PopenJob(argv, executable=executable, shell=shell, env=env, cwd=cwd) 470 | self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint) 471 | 472 | def call_native(self, module, method, argv, env, cwd, cb, 473 | echo, justprint=False, pycommandpath=None): 474 | """ 475 | Asynchronously call the native function 476 | """ 477 | 478 | job = PythonJob(module, method, argv, env, cwd, pycommandpath) 479 | self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint) 480 | 481 | @staticmethod 482 | def _waitany(condition): 483 | def _checkdone(): 484 | jobs = [] 485 | for c in ParallelContext._allcontexts: 486 | for i in range(0, len(c.running)): 487 | if c.running[i][0].done: 488 | jobs.append(c.running[i]) 489 | for j in jobs: 490 | if j in c.running: 491 | c.running.remove(j) 492 | return jobs 493 | 494 | # We must acquire the lock, and then check to see if any jobs have 495 | # finished. If we don't check after acquiring the lock it's possible 496 | # that all outstanding jobs will have completed before we wait and we'll 497 | # wait for notifications that have already occurred. 498 | condition.acquire() 499 | jobs = _checkdone() 500 | 501 | if jobs == []: 502 | condition.wait() 503 | jobs = _checkdone() 504 | 505 | condition.release() 506 | 507 | return jobs 508 | 509 | @staticmethod 510 | def spin(): 511 | """ 512 | Spin the 'event loop', and never return. 513 | """ 514 | 515 | while True: 516 | clist = list(ParallelContext._allcontexts) 517 | for c in clist: 518 | c.run() 519 | 520 | dowait = util.any((len(c.running) for c in ParallelContext._allcontexts)) 521 | if dowait: 522 | # Wait on local jobs first for perf 523 | for job, cb in ParallelContext._waitany(ParallelContext._condition): 524 | cb(job.exitcode) 525 | else: 526 | assert any(len(c.pending) for c in ParallelContext._allcontexts) 527 | 528 | def makedeferrable(usercb, **userkwargs): 529 | def cb(*args, **kwargs): 530 | kwargs.update(userkwargs) 531 | return usercb(*args, **kwargs) 532 | 533 | return cb 534 | 535 | _serialContext = None 536 | _parallelContext = None 537 | 538 | def getcontext(jcount): 539 | global _serialContext, _parallelContext 540 | if jcount == 1: 541 | if _serialContext is None: 542 | _serialContext = ParallelContext(1) 543 | return _serialContext 544 | else: 545 | if _parallelContext is None: 546 | _parallelContext = ParallelContext(jcount) 547 | return _parallelContext 548 | 549 | -------------------------------------------------------------------------------- /pymake/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def normaljoin(path, suffix): 4 | """ 5 | Combine the given path with the suffix, and normalize if necessary to shrink the path to avoid hitting path length limits 6 | """ 7 | result = os.path.join(path, suffix) 8 | if len(result) > 255: 9 | result = os.path.normpath(result) 10 | return result 11 | 12 | def joiniter(fd, it): 13 | """ 14 | Given an iterator that returns strings, write the words with a space in between each. 15 | """ 16 | 17 | it = iter(it) 18 | for i in it: 19 | fd.write(i) 20 | break 21 | 22 | for i in it: 23 | fd.write(' ') 24 | fd.write(i) 25 | 26 | def checkmsyscompat(): 27 | """For msys compatibility on windows, honor the SHELL environment variable, 28 | and if $MSYSTEM == MINGW32, run commands through $SHELL -c instead of 29 | letting Python use the system shell.""" 30 | if 'SHELL' in os.environ: 31 | shell = os.environ['SHELL'] 32 | elif 'MOZILLABUILD' in os.environ: 33 | shell = os.environ['MOZILLABUILD'] + '/msys/bin/sh.exe' 34 | elif 'COMSPEC' in os.environ: 35 | shell = os.environ['COMSPEC'] 36 | else: 37 | raise DataError("Can't find a suitable shell!") 38 | 39 | msys = False 40 | if 'MSYSTEM' in os.environ and os.environ['MSYSTEM'] == 'MINGW32': 41 | msys = True 42 | if not shell.lower().endswith(".exe"): 43 | shell += ".exe" 44 | return (shell, msys) 45 | 46 | if hasattr(str, 'partition'): 47 | def strpartition(str, token): 48 | return str.partition(token) 49 | 50 | def strrpartition(str, token): 51 | return str.rpartition(token) 52 | 53 | else: 54 | def strpartition(str, token): 55 | """Python 2.4 compatible str.partition""" 56 | 57 | offset = str.find(token) 58 | if offset == -1: 59 | return str, '', '' 60 | 61 | return str[:offset], token, str[offset + len(token):] 62 | 63 | def strrpartition(str, token): 64 | """Python 2.4 compatible str.rpartition""" 65 | 66 | offset = str.rfind(token) 67 | if offset == -1: 68 | return '', '', str 69 | 70 | return str[:offset], token, str[offset + len(token):] 71 | 72 | try: 73 | from __builtin__ import any 74 | except ImportError: 75 | def any(it): 76 | for i in it: 77 | if i: 78 | return True 79 | return False 80 | 81 | class _MostUsedItem(object): 82 | __slots__ = ('key', 'o', 'count') 83 | 84 | def __init__(self, key): 85 | self.key = key 86 | self.o = None 87 | self.count = 1 88 | 89 | def __repr__(self): 90 | return "MostUsedItem(key=%r, count=%i, o=%r)" % (self.key, self.count, self.o) 91 | 92 | class MostUsedCache(object): 93 | def __init__(self, capacity, creationfunc, verifyfunc): 94 | self.capacity = capacity 95 | self.cfunc = creationfunc 96 | self.vfunc = verifyfunc 97 | 98 | self.d = {} 99 | self.active = [] # lazily sorted! 100 | 101 | def setactive(self, item): 102 | if item in self.active: 103 | return 104 | 105 | if len(self.active) == self.capacity: 106 | self.active.sort(key=lambda i: i.count) 107 | old = self.active.pop(0) 108 | old.o = None 109 | # print "Evicting %s" % old.key 110 | 111 | self.active.append(item) 112 | 113 | def get(self, key): 114 | item = self.d.get(key, None) 115 | if item is None: 116 | item = _MostUsedItem(key) 117 | self.d[key] = item 118 | else: 119 | item.count += 1 120 | 121 | if item.o is not None and self.vfunc(key, item.o): 122 | return item.o 123 | 124 | item.o = self.cfunc(key) 125 | self.setactive(item) 126 | return item.o 127 | 128 | def verify(self): 129 | for k, v in self.d.items(): 130 | if v.o: 131 | assert v in self.active 132 | else: 133 | assert v not in self.active 134 | 135 | def debugitems(self): 136 | l = [i.key for i in self.active] 137 | l.sort() 138 | return l 139 | -------------------------------------------------------------------------------- /pymake/win32process.py: -------------------------------------------------------------------------------- 1 | from ctypes import windll, POINTER, byref, WinError, WINFUNCTYPE 2 | from ctypes.wintypes import HANDLE, DWORD, BOOL 3 | 4 | INFINITE = -1 5 | WAIT_FAILED = 0xFFFFFFFF 6 | 7 | LPDWORD = POINTER(DWORD) 8 | _GetExitCodeProcessProto = WINFUNCTYPE(BOOL, HANDLE, LPDWORD) 9 | _GetExitCodeProcess = _GetExitCodeProcessProto(("GetExitCodeProcess", windll.kernel32)) 10 | def GetExitCodeProcess(h): 11 | exitcode = DWORD() 12 | r = _GetExitCodeProcess(h, byref(exitcode)) 13 | if r is 0: 14 | raise WinError() 15 | return exitcode.value 16 | 17 | _WaitForMultipleObjectsProto = WINFUNCTYPE(DWORD, DWORD, POINTER(HANDLE), BOOL, DWORD) 18 | _WaitForMultipleObjects = _WaitForMultipleObjectsProto(("WaitForMultipleObjects", windll.kernel32)) 19 | 20 | def WaitForAnyProcess(processes): 21 | arrtype = HANDLE * len(processes) 22 | harray = arrtype(*(int(p._handle) for p in processes)) 23 | 24 | r = _WaitForMultipleObjects(len(processes), harray, False, INFINITE) 25 | if r == WAIT_FAILED: 26 | raise WinError() 27 | 28 | return processes[r], GetExitCodeProcess(int(processes[r]._handle)) <<8 29 | -------------------------------------------------------------------------------- /tests/automatic-variables.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | mkdir -p src/subd; \ 3 | mkdir subd; \ 4 | touch dummy; \ 5 | sleep 2; \ 6 | touch subd/test.out src/subd/test.in2; \ 7 | sleep 2; \ 8 | touch subd/test.out2 src/subd/test.in; \ 9 | sleep 2; \ 10 | touch subd/host_test.out subd/host_test.out2; \ 11 | sleep 2; \ 12 | touch host_prog; \ 13 | ) 14 | 15 | VPATH = src 16 | 17 | all: prog host_prog prog dir/ 18 | test "$@" = "all" 19 | test "$<" = "prog" 20 | test "$^" = "prog host_prog dir" 21 | test "$?" = "prog host_prog dir" 22 | test "$+" = "prog host_prog prog dir" 23 | test "$(@D)" = "." 24 | test "$(@F)" = "all" 25 | test "$($@ 75 | 76 | %.out2: %.in2 dummy 77 | @echo TEST_FAIL No need to remake 78 | 79 | .PHONY: all 80 | -------------------------------------------------------------------------------- /tests/bad-command-continuation.mk: -------------------------------------------------------------------------------- 1 | all: 2 | echo 'hello'\ 3 | TEST-PASS 4 | -------------------------------------------------------------------------------- /tests/call.mk: -------------------------------------------------------------------------------- 1 | test = $0 2 | reverse = $2 $1 3 | twice = $1$1 4 | sideeffect = $(shell echo "called$1:" >>dummyfile) 5 | 6 | all: 7 | test "$(call test)" = "test" 8 | test "$(call reverse,1,2)" = "2 1" 9 | # expansion happens *before* substitution, thank sanity 10 | test "$(call twice,$(sideeffect))" = "" 11 | test `cat dummyfile` = "called:" 12 | @echo TEST-PASS 13 | -------------------------------------------------------------------------------- /tests/cmd-stripdotslash.mk: -------------------------------------------------------------------------------- 1 | all: 2 | $(MAKE) -f $(TESTPATH)/cmd-stripdotslash.mk ./foo 3 | 4 | ./foo: 5 | @echo TEST-PASS 6 | -------------------------------------------------------------------------------- /tests/cmdgoals.mk: -------------------------------------------------------------------------------- 1 | default: 2 | test "$(MAKECMDGOALS)" = "" 3 | $(MAKE) -f $(TESTPATH)/cmdgoals.mk t1 t2 4 | @echo TEST-PASS 5 | 6 | t1: 7 | test "$(MAKECMDGOALS)" = "t1 t2" 8 | 9 | t2: 10 | -------------------------------------------------------------------------------- /tests/commandmodifiers.mk: -------------------------------------------------------------------------------- 1 | define COMMAND 2 | $(1) 3 | $(1) 4 | 5 | endef 6 | 7 | all: 8 | $(call COMMAND,@true #TEST-FAIL) 9 | $(call COMMAND,-exit 4) 10 | $(call COMMAND,@-exit 1 # TEST-FAIL) 11 | $(call COMMAND,-@exit 1 # TEST-FAIL) 12 | $(call COMMAND,+exit 0) 13 | $(call COMMAND,+-exit 1) 14 | $(call COMMAND,@+exit 0 # TEST-FAIL) 15 | $(call COMMAND,+@exit 0 # TEST-FAIL) 16 | $(call COMMAND,-+@exit 1 # TEST-FAIL) 17 | $(call COMMAND,+-@exit 1 # TEST-FAIL) 18 | $(call COMMAND,@+-exit 1 # TEST-FAIL) 19 | $(call COMMAND,@+-@+-exit 1 # TEST-FAIL) 20 | $(call COMMAND,@@++exit 0 # TEST-FAIL) 21 | @echo TEST-PASS 22 | -------------------------------------------------------------------------------- /tests/comment-parsing.mk: -------------------------------------------------------------------------------- 1 | # where do comments take effect? 2 | 3 | VAR = val1 # comment 4 | VAR2 = lit2\#hash 5 | VAR2_1 = lit2.1\\\#hash 6 | VAR3 = val3 7 | VAR4 = lit4\\#backslash 8 | VAR4_1 = lit4\\\\#backslash 9 | VAR5 = lit5\char 10 | VAR6 = lit6\\char 11 | VAR7 = lit7\\ 12 | VAR8 = lit8\\\\ 13 | VAR9 = lit9\\\\extra 14 | # This comment extends to the next line \ 15 | VAR3 = ignored 16 | 17 | all: 18 | test "$(VAR)" = "val1 " 19 | test "$(VAR2)" = "lit2#hash" 20 | test '$(VAR2_1)' = 'lit2.1\#hash' 21 | test "$(VAR3)" = "val3" 22 | test '$(VAR4)' = 'lit4\' 23 | test '$(VAR4_1)' = 'lit4\\' 24 | test '$(VAR5)' = 'lit5\char' 25 | test '$(VAR6)' = 'lit6\\char' 26 | test '$(VAR7)' = 'lit7\\' 27 | test '$(VAR8)' = 'lit8\\\\' 28 | test '$(VAR9)' = 'lit9\\\\extra' 29 | @echo "TEST-PASS" 30 | -------------------------------------------------------------------------------- /tests/continuations-in-functions.mk: -------------------------------------------------------------------------------- 1 | all: 2 | test 'Hello world.' = '$(if 1,Hello \ 3 | world.)' 4 | test '(Hello world.)' != '(Hello \ 5 | world.)' 6 | @echo TEST-PASS 7 | -------------------------------------------------------------------------------- /tests/datatests.py: -------------------------------------------------------------------------------- 1 | import pymake.data, pymake.functions, pymake.util 2 | import unittest 3 | import re 4 | 5 | 6 | def multitest(cls): 7 | for name in cls.testdata.keys(): 8 | def m(self, name=name): 9 | return self.runSingle(*self.testdata[name]) 10 | 11 | setattr(cls, 'test_%s' % name, m) 12 | return cls 13 | 14 | class SplitWordsTest(unittest.TestCase): 15 | testdata = ( 16 | (' test test.c test.o ', ['test', 'test.c', 'test.o']), 17 | ('\ttest\t test.c \ntest.o', ['test', 'test.c', 'test.o']), 18 | ) 19 | 20 | def runTest(self): 21 | for s, e in self.testdata: 22 | w = s.split() 23 | self.assertEqual(w, e, 'splitwords(%r)' % (s,)) 24 | 25 | class GetPatSubstTest(unittest.TestCase): 26 | testdata = ( 27 | ('%.c', '%.o', ' test test.c test.o ', 'test test.o test.o'), 28 | ('%', '%.o', ' test.c test.o ', 'test.c.o test.o.o'), 29 | ('foo', 'bar', 'test foo bar', 'test bar bar'), 30 | ('foo', '%bar', 'test foo bar', 'test %bar bar'), 31 | ('%', 'perc_%', 'path', 'perc_path'), 32 | ('\\%', 'sub%', 'p %', 'p sub%'), 33 | ('%.c', '\\%%.o', 'foo.c bar.o baz.cpp', '%foo.o bar.o baz.cpp'), 34 | ) 35 | 36 | def runTest(self): 37 | for s, r, d, e in self.testdata: 38 | words = d.split() 39 | p = pymake.data.Pattern(s) 40 | a = ' '.join((p.subst(r, word, False) 41 | for word in words)) 42 | self.assertEqual(a, e, 'Pattern(%r).subst(%r, %r)' % (s, r, d)) 43 | 44 | class LRUTest(unittest.TestCase): 45 | # getkey, expected, funccount, debugitems 46 | expected = ( 47 | (0, '', 1, (0,)), 48 | (0, '', 2, (0,)), 49 | (1, ' ', 3, (1, 0)), 50 | (1, ' ', 3, (1, 0)), 51 | (0, '', 4, (0, 1)), 52 | (2, ' ', 5, (2, 0, 1)), 53 | (1, ' ', 5, (1, 2, 0)), 54 | (3, ' ', 6, (3, 1, 2)), 55 | ) 56 | 57 | def spaceFunc(self, l): 58 | self.funccount += 1 59 | return ''.ljust(l) 60 | 61 | def runTest(self): 62 | self.funccount = 0 63 | c = pymake.util.LRUCache(3, self.spaceFunc, lambda k, v: k % 2) 64 | self.assertEqual(tuple(c.debugitems()), ()) 65 | 66 | for i in range(0, len(self.expected)): 67 | k, e, fc, di = self.expected[i] 68 | 69 | v = c.get(k) 70 | self.assertEqual(v, e) 71 | self.assertEqual(self.funccount, fc, 72 | "funccount, iteration %i, got %i expected %i" % (i, self.funccount, fc)) 73 | goti = tuple(c.debugitems()) 74 | self.assertEqual(goti, di, 75 | "debugitems, iteration %i, got %r expected %r" % (i, goti, di)) 76 | 77 | class EqualityTest(unittest.TestCase): 78 | def test_string_expansion(self): 79 | s1 = pymake.data.StringExpansion('foo bar', None) 80 | s2 = pymake.data.StringExpansion('foo bar', None) 81 | 82 | self.assertEqual(s1, s2) 83 | 84 | def test_expansion_simple(self): 85 | s1 = pymake.data.Expansion(None) 86 | s2 = pymake.data.Expansion(None) 87 | 88 | self.assertEqual(s1, s2) 89 | 90 | s1.appendstr('foo') 91 | s2.appendstr('foo') 92 | self.assertEqual(s1, s2) 93 | 94 | def test_expansion_string_finish(self): 95 | """Adjacent strings should normalize to same value.""" 96 | s1 = pymake.data.Expansion(None) 97 | s2 = pymake.data.Expansion(None) 98 | 99 | s1.appendstr('foo') 100 | s2.appendstr('foo') 101 | 102 | s1.appendstr(' bar') 103 | s1.appendstr(' baz') 104 | s2.appendstr(' bar baz') 105 | 106 | self.assertEqual(s1, s2) 107 | 108 | def test_function(self): 109 | s1 = pymake.data.Expansion(None) 110 | s2 = pymake.data.Expansion(None) 111 | 112 | n1 = pymake.data.StringExpansion('FOO', None) 113 | n2 = pymake.data.StringExpansion('FOO', None) 114 | 115 | v1 = pymake.functions.VariableRef(None, n1) 116 | v2 = pymake.functions.VariableRef(None, n2) 117 | 118 | s1.appendfunc(v1) 119 | s2.appendfunc(v2) 120 | 121 | self.assertEqual(s1, s2) 122 | 123 | 124 | class StringExpansionTest(unittest.TestCase): 125 | def test_base_expansion_interface(self): 126 | s1 = pymake.data.StringExpansion('FOO', None) 127 | 128 | self.assertTrue(s1.is_static_string) 129 | 130 | funcs = list(s1.functions()) 131 | self.assertEqual(len(funcs), 0) 132 | 133 | funcs = list(s1.functions(True)) 134 | self.assertEqual(len(funcs), 0) 135 | 136 | refs = list(s1.variable_references()) 137 | self.assertEqual(len(refs), 0) 138 | 139 | 140 | class ExpansionTest(unittest.TestCase): 141 | def test_is_static_string(self): 142 | e1 = pymake.data.Expansion() 143 | e1.appendstr('foo') 144 | 145 | self.assertTrue(e1.is_static_string) 146 | 147 | e1.appendstr('bar') 148 | self.assertTrue(e1.is_static_string) 149 | 150 | vname = pymake.data.StringExpansion('FOO', None) 151 | func = pymake.functions.VariableRef(None, vname) 152 | 153 | e1.appendfunc(func) 154 | 155 | self.assertFalse(e1.is_static_string) 156 | 157 | def test_get_functions(self): 158 | e1 = pymake.data.Expansion() 159 | e1.appendstr('foo') 160 | 161 | vname1 = pymake.data.StringExpansion('FOO', None) 162 | vname2 = pymake.data.StringExpansion('BAR', None) 163 | 164 | func1 = pymake.functions.VariableRef(None, vname1) 165 | func2 = pymake.functions.VariableRef(None, vname2) 166 | 167 | e1.appendfunc(func1) 168 | e1.appendfunc(func2) 169 | 170 | funcs = list(e1.functions()) 171 | self.assertEqual(len(funcs), 2) 172 | 173 | func3 = pymake.functions.SortFunction(None) 174 | func3.append(vname1) 175 | 176 | e1.appendfunc(func3) 177 | 178 | funcs = list(e1.functions()) 179 | self.assertEqual(len(funcs), 3) 180 | 181 | refs = list(e1.variable_references()) 182 | self.assertEqual(len(refs), 2) 183 | 184 | def test_get_functions_descend(self): 185 | e1 = pymake.data.Expansion() 186 | vname1 = pymake.data.StringExpansion('FOO', None) 187 | func1 = pymake.functions.VariableRef(None, vname1) 188 | e2 = pymake.data.Expansion() 189 | e2.appendfunc(func1) 190 | 191 | func2 = pymake.functions.SortFunction(None) 192 | func2.append(e2) 193 | 194 | e1.appendfunc(func2) 195 | 196 | funcs = list(e1.functions()) 197 | self.assertEqual(len(funcs), 1) 198 | 199 | funcs = list(e1.functions(True)) 200 | self.assertEqual(len(funcs), 2) 201 | 202 | self.assertTrue(isinstance(funcs[0], pymake.functions.SortFunction)) 203 | 204 | def test_is_filesystem_dependent(self): 205 | e = pymake.data.Expansion() 206 | vname1 = pymake.data.StringExpansion('FOO', None) 207 | func1 = pymake.functions.VariableRef(None, vname1) 208 | e.appendfunc(func1) 209 | 210 | self.assertFalse(e.is_filesystem_dependent) 211 | 212 | func2 = pymake.functions.WildcardFunction(None) 213 | func2.append(vname1) 214 | e.appendfunc(func2) 215 | 216 | self.assertTrue(e.is_filesystem_dependent) 217 | 218 | def test_is_filesystem_dependent_descend(self): 219 | sort = pymake.functions.SortFunction(None) 220 | wildcard = pymake.functions.WildcardFunction(None) 221 | 222 | e = pymake.data.StringExpansion('foo/*', None) 223 | wildcard.append(e) 224 | 225 | e = pymake.data.Expansion(None) 226 | e.appendfunc(wildcard) 227 | 228 | sort.append(e) 229 | 230 | e = pymake.data.Expansion(None) 231 | e.appendfunc(sort) 232 | 233 | self.assertTrue(e.is_filesystem_dependent) 234 | 235 | 236 | if __name__ == '__main__': 237 | unittest.main() 238 | -------------------------------------------------------------------------------- /tests/default-goal-set-first.mk: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := default 2 | 3 | not-default: 4 | @echo TEST-FAIL did not run default rule 5 | 6 | default: 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/default-goal.mk: -------------------------------------------------------------------------------- 1 | not-default: 2 | @echo TEST-FAIL did not run default rule 3 | 4 | default: 5 | @echo $(if $(filter not-default,$(INTERMEDIATE_DEFAULT_GOAL)),TEST-PASS,TEST-FAIL .DEFAULT_GOAL not set by $(MAKE)) 6 | 7 | INTERMEDIATE_DEFAULT_GOAL := $(.DEFAULT_GOAL) 8 | .DEFAULT_GOAL := default 9 | -------------------------------------------------------------------------------- /tests/default-target.mk: -------------------------------------------------------------------------------- 1 | test: VAR = value 2 | 3 | %.do: 4 | @echo TEST-FAIL: ran target "$@", should have run "all" 5 | 6 | .PHONY: test 7 | 8 | all: 9 | @echo TEST-PASS: the default target is all 10 | 11 | test: 12 | @echo TEST-FAIL: ran target "$@", should have run "all" 13 | 14 | test.do: 15 | -------------------------------------------------------------------------------- /tests/default-target2.mk: -------------------------------------------------------------------------------- 1 | test.foo: %.foo: 2 | test "$@" = "test.foo" 3 | @echo TEST-PASS made test.foo by default 4 | 5 | all: 6 | @echo TEST-FAIL made $@, should have made test.foo 7 | -------------------------------------------------------------------------------- /tests/define-directive.mk: -------------------------------------------------------------------------------- 1 | define COMMANDS 2 | shellvar=hello 3 | test "$$shellvar" != "hello" 4 | endef 5 | 6 | define COMMANDS2 7 | shellvar=hello; \ 8 | test "$$shellvar" = "hello" 9 | endef 10 | 11 | define VARWITHCOMMENT # comment 12 | value 13 | endef 14 | 15 | define TEST3 16 | whitespace 17 | endef 18 | 19 | define TEST4 20 | define TEST5 21 | random 22 | endef 23 | endef 24 | 25 | ifdef TEST5 26 | $(error TEST5 should not be set) 27 | endif 28 | 29 | define TEST6 30 | define TEST7 31 | random 32 | endef 33 | endef 34 | 35 | ifdef TEST7 36 | $(error TEST7 should not be set) 37 | endif 38 | 39 | define TEST8 40 | is this # a comment? 41 | endef 42 | 43 | ifneq ($(TEST8),is this \# a comment?) 44 | $(error TEST8 value not expected: $(TEST8)) 45 | endif 46 | 47 | # A backslash continuation "hides" the endef 48 | define TEST9 49 | value \ 50 | endef 51 | endef 52 | 53 | # Test ridiculous spacing 54 | define TEST10 55 | define TEST11 56 | baz 57 | endef 58 | define TEST12 59 | foo 60 | endef 61 | endef 62 | 63 | all: 64 | $(COMMANDS) 65 | $(COMMANDS2) 66 | test '$(VARWITHCOMMENT)' = 'value' 67 | test '$(COMMANDS2)' = 'shellvar=hello; test "$$shellvar" = "hello"' 68 | test "$(TEST3)" = " whitespace" 69 | @echo TEST-PASS 70 | -------------------------------------------------------------------------------- /tests/depfailed.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | all: foo.out foo.in 4 | @echo TEST-PASS 5 | -------------------------------------------------------------------------------- /tests/depfailedj.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | #T commandline: ['-j4'] 3 | 4 | $(shell touch foo.in) 5 | 6 | all: foo.in foo.out missing 7 | @echo TEST-PASS 8 | 9 | %.out: %.in 10 | cp $< $@ 11 | -------------------------------------------------------------------------------- /tests/diamond-deps.mk: -------------------------------------------------------------------------------- 1 | # If the dependency graph includes a diamond dependency, we should only remake 2 | # once! 3 | 4 | all: depA depB 5 | cat testfile 6 | test `cat testfile` = "data"; 7 | @echo TEST-PASS 8 | 9 | depA: testfile 10 | depB: testfile 11 | 12 | testfile: 13 | printf "data" >>$@ 14 | -------------------------------------------------------------------------------- /tests/dotslash-dir.mk: -------------------------------------------------------------------------------- 1 | #T grep-for: "dotslash-built" 2 | .PHONY: $(dir foo) 3 | 4 | all: $(dir foo) 5 | @echo TEST-PASS 6 | 7 | $(dir foo): 8 | @echo dotslash-built 9 | -------------------------------------------------------------------------------- /tests/dotslash-parse.mk: -------------------------------------------------------------------------------- 1 | ./: 2 | 3 | # This is merely a test to see that pymake doesn't choke on parsing ./ 4 | $(info TEST-PASS) 5 | -------------------------------------------------------------------------------- /tests/dotslash-phony.mk: -------------------------------------------------------------------------------- 1 | .PHONY: ./ 2 | ./: 3 | @echo TEST-PASS 4 | -------------------------------------------------------------------------------- /tests/dotslash.mk: -------------------------------------------------------------------------------- 1 | $(shell touch foo.in) 2 | 3 | all: foo.out 4 | test "$(wildcard ./*.in)" = "./foo.in" 5 | @echo TEST-PASS 6 | 7 | ./%.out: %.in 8 | test "$@" = "foo.out" 9 | cp $< $@ 10 | -------------------------------------------------------------------------------- /tests/doublecolon-exists.mk: -------------------------------------------------------------------------------- 1 | $(shell touch foo.testfile1 foo.testfile2) 2 | 3 | # when a rule has commands and no prerequisites, should it be executed? 4 | # double-colon: yes 5 | # single-colon: no 6 | 7 | all: foo.testfile1 foo.testfile2 8 | test "$$(cat foo.testfile1)" = "" 9 | test "$$(cat foo.testfile2)" = "remade:foo.testfile2" 10 | @echo TEST-PASS 11 | 12 | foo.testfile1: 13 | @echo TEST-FAIL 14 | 15 | foo.testfile2:: 16 | printf "remade:$@"> $@ 17 | -------------------------------------------------------------------------------- /tests/doublecolon-priordeps.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j3'] 2 | 3 | # All *prior* dependencies of a doublecolon rule must be satisfied before 4 | # subsequent commands are run. 5 | 6 | all:: target1 7 | 8 | all:: target2 9 | test -f target1 10 | @echo TEST-PASS 11 | 12 | target1: 13 | touch starting-$@ 14 | sleep 1 15 | touch $@ 16 | 17 | target2: 18 | sleep 0.1 19 | test -f starting-target1 20 | -------------------------------------------------------------------------------- /tests/doublecolon-remake.mk: -------------------------------------------------------------------------------- 1 | $(shell touch somefile) 2 | 3 | all:: somefile 4 | @echo TEST-PASS 5 | -------------------------------------------------------------------------------- /tests/dynamic-var.mk: -------------------------------------------------------------------------------- 1 | # The *name* of variables can be constructed dynamically. 2 | 3 | VARNAME = FOOBAR 4 | 5 | $(VARNAME) = foovalue 6 | $(VARNAME)2 = foo2value 7 | 8 | $(VARNAME:%BAR=%BAM) = foobam 9 | 10 | all: 11 | test "$(FOOBAR)" = "foovalue" 12 | test "$(flavor FOOBAZ)" = "undefined" 13 | test "$(FOOBAR2)" = "bazvalue" 14 | test "$(FOOBAM)" = "foobam" 15 | @echo TEST-PASS 16 | 17 | VARNAME = FOOBAZ 18 | FOOBAR2 = bazvalue 19 | -------------------------------------------------------------------------------- /tests/empty-arg.mk: -------------------------------------------------------------------------------- 1 | all: 2 | @ sh -c 'if [ $$# = 3 ] ; then echo TEST-PASS; else echo TEST-FAIL; fi' -- a "" b 3 | -------------------------------------------------------------------------------- /tests/empty-command-semicolon.mk: -------------------------------------------------------------------------------- 1 | all: 2 | @echo TEST-PASS 3 | 4 | foo: ; 5 | 6 | -------------------------------------------------------------------------------- /tests/empty-rule.mk: -------------------------------------------------------------------------------- 1 | all: obj 2 | $(MAKE) -f $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) obj 3 | 4 | obj: header 5 | if [ -e obj ]; then echo TEST-FAIL; fi; 6 | touch obj 7 | 8 | header: cpp 9 | 10 | cpp: 11 | touch header 12 | touch cpp 13 | -------------------------------------------------------------------------------- /tests/empty-with-deps.mk: -------------------------------------------------------------------------------- 1 | default.test: default.c 2 | 3 | default.c: 4 | @echo TEST-PASS 5 | -------------------------------------------------------------------------------- /tests/env-var-append.mk: -------------------------------------------------------------------------------- 1 | #T environment: {'FOO': 'TEST'} 2 | 3 | FOO += $(BAR) 4 | BAR := PASS 5 | 6 | all: 7 | @echo $(subst $(NULL) ,-,$(FOO)) 8 | -------------------------------------------------------------------------------- /tests/env-var-append2.mk: -------------------------------------------------------------------------------- 1 | #T environment: {'FOO': '$(BAZ)'} 2 | 3 | FOO += $(BAR) 4 | BAR := PASS 5 | BAZ := TEST 6 | 7 | all: 8 | @echo $(subst $(NULL) ,-,$(FOO)) 9 | -------------------------------------------------------------------------------- /tests/eof-continuation.mk: -------------------------------------------------------------------------------- 1 | all: 2 | test '$(TESTVAR)' = 'testval\' 3 | @echo TEST-PASS 4 | 5 | TESTVAR = testval\ -------------------------------------------------------------------------------- /tests/escape-chars.mk: -------------------------------------------------------------------------------- 1 | space = $(NULL) $(NULL) 2 | hello$(space)world$(space) = hellovalue 3 | 4 | A = aval 5 | 6 | VAR = value1\\ 7 | VARAWFUL = value1\\#comment 8 | VAR2 = value2 9 | VAR3 = test\$A 10 | VAR4 = value4\\value5 11 | 12 | VAR5 = value1\\ \ \ 13 | value2 14 | 15 | EPERCENT = \% 16 | 17 | all: 18 | test "$(hello world )" = "hellovalue" 19 | test "$(VAR)" = "value1\\" 20 | test '$(VARAWFUL)' = 'value1\' 21 | test "$(VAR2)" = "value2" 22 | test "$(VAR3)" = "test\aval" 23 | test "$(VAR4)" = "value4\\value5" 24 | test "$(VAR5)" = "value1\\ \ value2" 25 | test "$(EPERCENT)" = "\%" 26 | @echo TEST-PASS 27 | -------------------------------------------------------------------------------- /tests/escaped-continuation.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | all: 4 | echo "Hello" \\ 5 | test "world" = "not!" 6 | @echo TEST-PASS 7 | -------------------------------------------------------------------------------- /tests/eval-duringexecute.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # Once parsing is finished, recursive expansion in commands are not allowed to create any new rules (it may only set variables) 4 | 5 | define MORERULE 6 | all: 7 | @echo TEST-FAIL 8 | endef 9 | 10 | all: 11 | $(eval $(MORERULE)) 12 | @echo done 13 | -------------------------------------------------------------------------------- /tests/eval.mk: -------------------------------------------------------------------------------- 1 | TESTVAR = val1 2 | 3 | $(eval TESTVAR = val2) 4 | 5 | all: 6 | test "$(TESTVAR)" = "val2" 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/exit-code.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | all: 4 | exit 1 5 | @echo TEST-PASS 6 | -------------------------------------------------------------------------------- /tests/file-functions-symlinks.mk: -------------------------------------------------------------------------------- 1 | #T returncode-on: {'win32': 2} 2 | $(shell \ 3 | touch test.file; \ 4 | ln -s test.file test.symlink; \ 5 | ln -s test.missing missing.symlink; \ 6 | touch .testhidden; \ 7 | mkdir foo; \ 8 | touch foo/testfile; \ 9 | ln -s foo symdir; \ 10 | ) 11 | 12 | all: 13 | test "$(abspath test.file test.symlink)" = "$(CURDIR)/test.file $(CURDIR)/test.symlink" 14 | test "$(realpath test.file test.symlink)" = "$(CURDIR)/test.file $(CURDIR)/test.file" 15 | test "$(sort $(wildcard *))" = "foo symdir test.file test.symlink" 16 | test "$(sort $(wildcard .*))" = ". .. .testhidden" 17 | test "$(sort $(wildcard test*))" = "test.file test.symlink" 18 | test "$(sort $(wildcard foo/*))" = "foo/testfile" 19 | test "$(sort $(wildcard ./*))" = "./foo ./symdir ./test.file ./test.symlink" 20 | test "$(sort $(wildcard f?o/*))" = "foo/testfile" 21 | test "$(sort $(wildcard */*))" = "foo/testfile symdir/testfile" 22 | @echo TEST-PASS 23 | -------------------------------------------------------------------------------- /tests/file-functions.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | touch test.file; \ 3 | touch .testhidden; \ 4 | mkdir foo; \ 5 | touch foo/testfile; \ 6 | ) 7 | 8 | all: 9 | test "$(abspath test.file)" = "$(CURDIR)/test.file" 10 | test "$(realpath test.file)" = "$(CURDIR)/test.file" 11 | test "$(sort $(wildcard *))" = "foo test.file" 12 | # commented out because GNU make matches . and .. while python doesn't, and I don't 13 | # care enough 14 | # test "$(sort $(wildcard .*))" = ". .. .testhidden" 15 | test "$(sort $(wildcard test*))" = "test.file" 16 | test "$(sort $(wildcard foo/*))" = "foo/testfile" 17 | test "$(sort $(wildcard ./*))" = "./foo ./test.file" 18 | test "$(sort $(wildcard f?o/*))" = "foo/testfile" 19 | @echo TEST-PASS 20 | -------------------------------------------------------------------------------- /tests/foreach-local-variable.mk: -------------------------------------------------------------------------------- 1 | # This test ensures that a local variable in a $(foreach) is bound to 2 | # the local value, not a global value. 3 | i := dummy 4 | 5 | all: 6 | test "$(foreach i,foo bar,found:$(i))" = "found:foo found:bar" 7 | test "$(i)" = "dummy" 8 | @echo TEST-PASS 9 | -------------------------------------------------------------------------------- /tests/formattingtests.py: -------------------------------------------------------------------------------- 1 | # This file contains test code for the formatting of parsed statements back to 2 | # make file "source." It essentially verifies to to_source() functions 3 | # scattered across the tree. 4 | 5 | import glob 6 | import logging 7 | import os.path 8 | import unittest 9 | 10 | from pymake.data import Expansion 11 | from pymake.data import StringExpansion 12 | from pymake.functions import BasenameFunction 13 | from pymake.functions import SubstitutionRef 14 | from pymake.functions import VariableRef 15 | from pymake.functions import WordlistFunction 16 | from pymake.parserdata import Include 17 | from pymake.parserdata import SetVariable 18 | from pymake.parser import parsestring 19 | from pymake.parser import SyntaxError 20 | 21 | class TestBase(unittest.TestCase): 22 | pass 23 | 24 | class VariableRefTest(TestBase): 25 | def test_string_name(self): 26 | e = StringExpansion('foo', None) 27 | v = VariableRef(None, e) 28 | 29 | self.assertEqual(v.to_source(), '$(foo)') 30 | 31 | def test_special_variable(self): 32 | e = StringExpansion('<', None) 33 | v = VariableRef(None, e) 34 | 35 | self.assertEqual(v.to_source(), '$<') 36 | 37 | def test_expansion_simple(self): 38 | e = Expansion() 39 | e.appendstr('foo') 40 | e.appendstr('bar') 41 | 42 | v = VariableRef(None, e) 43 | 44 | self.assertEqual(v.to_source(), '$(foobar)') 45 | 46 | class StandardFunctionTest(TestBase): 47 | def test_basename(self): 48 | e1 = StringExpansion('foo', None) 49 | v = VariableRef(None, e1) 50 | e2 = Expansion(None) 51 | e2.appendfunc(v) 52 | 53 | b = BasenameFunction(None) 54 | b.append(e2) 55 | 56 | self.assertEqual(b.to_source(), '$(basename $(foo))') 57 | 58 | def test_wordlist(self): 59 | e1 = StringExpansion('foo', None) 60 | e2 = StringExpansion('bar ', None) 61 | e3 = StringExpansion(' baz', None) 62 | 63 | w = WordlistFunction(None) 64 | w.append(e1) 65 | w.append(e2) 66 | w.append(e3) 67 | 68 | self.assertEqual(w.to_source(), '$(wordlist foo,bar , baz)') 69 | 70 | def test_curly_brackets(self): 71 | e1 = Expansion(None) 72 | e1.appendstr('foo') 73 | 74 | e2 = Expansion(None) 75 | e2.appendstr('foo ( bar') 76 | 77 | f = WordlistFunction(None) 78 | f.append(e1) 79 | f.append(e2) 80 | 81 | self.assertEqual(f.to_source(), '${wordlist foo,foo ( bar}') 82 | 83 | class StringExpansionTest(TestBase): 84 | def test_simple(self): 85 | e = StringExpansion('foobar', None) 86 | self.assertEqual(e.to_source(), 'foobar') 87 | 88 | e = StringExpansion('$var', None) 89 | self.assertEqual(e.to_source(), '$var') 90 | 91 | def test_escaping(self): 92 | e = StringExpansion('$var', None) 93 | self.assertEqual(e.to_source(escape_variables=True), '$$var') 94 | 95 | e = StringExpansion('this is # not a comment', None) 96 | self.assertEqual(e.to_source(escape_comments=True), 97 | 'this is \# not a comment') 98 | 99 | def test_empty(self): 100 | e = StringExpansion('', None) 101 | self.assertEqual(e.to_source(), '') 102 | 103 | e = StringExpansion(' ', None) 104 | self.assertEqual(e.to_source(), ' ') 105 | 106 | class ExpansionTest(TestBase): 107 | def test_single_string(self): 108 | e = Expansion() 109 | e.appendstr('foo') 110 | 111 | self.assertEqual(e.to_source(), 'foo') 112 | 113 | def test_multiple_strings(self): 114 | e = Expansion() 115 | e.appendstr('hello') 116 | e.appendstr('world') 117 | 118 | self.assertEqual(e.to_source(), 'helloworld') 119 | 120 | def test_string_escape(self): 121 | e = Expansion() 122 | e.appendstr('$var') 123 | self.assertEqual(e.to_source(), '$var') 124 | self.assertEqual(e.to_source(escape_variables=True), '$$var') 125 | 126 | e = Expansion() 127 | e.appendstr('foo') 128 | e.appendstr(' $bar') 129 | self.assertEqual(e.to_source(escape_variables=True), 'foo $$bar') 130 | 131 | class SubstitutionRefTest(TestBase): 132 | def test_simple(self): 133 | name = StringExpansion('foo', None) 134 | c = StringExpansion('%.c', None) 135 | o = StringExpansion('%.o', None) 136 | s = SubstitutionRef(None, name, c, o) 137 | 138 | self.assertEqual(s.to_source(), '$(foo:%.c=%.o)') 139 | 140 | class SetVariableTest(TestBase): 141 | def test_simple(self): 142 | v = SetVariable(StringExpansion('foo', None), '=', 'bar', None, None) 143 | self.assertEqual(v.to_source(), 'foo = bar') 144 | 145 | def test_multiline(self): 146 | s = 'hello\nworld' 147 | foo = StringExpansion('FOO', None) 148 | 149 | v = SetVariable(foo, '=', s, None, None) 150 | 151 | self.assertEqual(v.to_source(), 'define FOO\nhello\nworld\nendef') 152 | 153 | def test_multiline_immediate(self): 154 | source = 'define FOO :=\nhello\nworld\nendef' 155 | 156 | statements = parsestring(source, 'foo.mk') 157 | self.assertEqual(statements.to_source(), source) 158 | 159 | def test_target_specific(self): 160 | foo = StringExpansion('FOO', None) 161 | bar = StringExpansion('BAR', None) 162 | 163 | v = SetVariable(foo, '+=', 'value', None, bar) 164 | 165 | self.assertEqual(v.to_source(), 'BAR: FOO += value') 166 | 167 | class IncludeTest(TestBase): 168 | def test_include(self): 169 | e = StringExpansion('rules.mk', None) 170 | i = Include(e, True, False) 171 | self.assertEqual(i.to_source(), 'include rules.mk') 172 | 173 | i = Include(e, False, False) 174 | self.assertEqual(i.to_source(), '-include rules.mk') 175 | 176 | class IfdefTest(TestBase): 177 | def test_simple(self): 178 | source = 'ifdef FOO\nbar := $(value)\nendif' 179 | 180 | statements = parsestring(source, 'foo.mk') 181 | self.assertEqual(statements[0].to_source(), source) 182 | 183 | def test_nested(self): 184 | source = 'ifdef FOO\nifdef BAR\nhello = world\nendif\nendif' 185 | 186 | statements = parsestring(source, 'foo.mk') 187 | self.assertEqual(statements[0].to_source(), source) 188 | 189 | def test_negation(self): 190 | source = 'ifndef FOO\nbar += value\nendif' 191 | 192 | statements = parsestring(source, 'foo.mk') 193 | self.assertEqual(statements[0].to_source(), source) 194 | 195 | class IfeqTest(TestBase): 196 | def test_simple(self): 197 | source = 'ifeq ($(foo),bar)\nhello = $(world)\nendif' 198 | 199 | statements = parsestring(source, 'foo.mk') 200 | self.assertEqual(statements[0].to_source(), source) 201 | 202 | def test_negation(self): 203 | source = 'ifneq (foo,bar)\nhello = world\nendif' 204 | 205 | statements = parsestring(source, 'foo.mk') 206 | self.assertEqual(statements.to_source(), source) 207 | 208 | class ConditionBlocksTest(TestBase): 209 | def test_mixed_conditions(self): 210 | source = 'ifdef FOO\nifeq ($(FOO),bar)\nvar += $(value)\nendif\nendif' 211 | 212 | statements = parsestring(source, 'foo.mk') 213 | self.assertEqual(statements.to_source(), source) 214 | 215 | def test_extra_statements(self): 216 | source = 'ifdef FOO\nF := 1\nifdef BAR\nB += 1\nendif\nC = 1\nendif' 217 | 218 | statements = parsestring(source, 'foo.mk') 219 | self.assertEqual(statements.to_source(), source) 220 | 221 | def test_whitespace_preservation(self): 222 | source = "ifeq ' x' 'x '\n$(error stripping)\nendif" 223 | 224 | statements = parsestring(source, 'foo.mk') 225 | self.assertEqual(statements.to_source(), source) 226 | 227 | source = 'ifneq (x , x)\n$(error stripping)\nendif' 228 | statements = parsestring(source, 'foo.mk') 229 | self.assertEqual(statements.to_source(), 230 | 'ifneq (x,x)\n$(error stripping)\nendif') 231 | 232 | class MakefileCorupusTest(TestBase): 233 | """Runs the make files from the pymake corpus through the formatter. 234 | 235 | All the above tests are child's play compared to this. 236 | """ 237 | 238 | # Our reformatting isn't perfect. We ignore files with known failures until 239 | # we make them work. 240 | # TODO Address these formatting corner cases. 241 | _IGNORE_FILES = [ 242 | # We are thrown off by backslashes at end of lines. 243 | 'comment-parsing.mk', 244 | 'escape-chars.mk', 245 | 'include-notfound.mk', 246 | ] 247 | 248 | def _get_test_files(self): 249 | ourdir = os.path.dirname(os.path.abspath(__file__)) 250 | 251 | for makefile in glob.glob(os.path.join(ourdir, '*.mk')): 252 | if os.path.basename(makefile) in self._IGNORE_FILES: 253 | continue 254 | 255 | source = None 256 | with open(makefile, 'rU') as fh: 257 | source = fh.read() 258 | 259 | try: 260 | yield (makefile, source, parsestring(source, makefile)) 261 | except SyntaxError: 262 | continue 263 | 264 | def test_reparse_consistency(self): 265 | for filename, source, statements in self._get_test_files(): 266 | reformatted = statements.to_source() 267 | 268 | # We should be able to parse the reformatted source fine. 269 | new_statements = parsestring(reformatted, filename) 270 | 271 | # If we do the formatting again, the representation shouldn't 272 | # change. i.e. the only lossy change should be the original 273 | # (whitespace and some semantics aren't preserved). 274 | reformatted_again = new_statements.to_source() 275 | self.assertEqual(reformatted, reformatted_again, 276 | '%s has lossless reformat.' % filename) 277 | 278 | self.assertEqual(len(statements), len(new_statements)) 279 | 280 | for i in range(0, len(statements)): 281 | original = statements[i] 282 | formatted = new_statements[i] 283 | 284 | self.assertEqual(original, formatted, '%s %d: %s != %s' % (filename, 285 | i, original, formatted)) 286 | 287 | if __name__ == '__main__': 288 | logging.basicConfig(level=logging.DEBUG) 289 | unittest.main() 290 | -------------------------------------------------------------------------------- /tests/func-refs.mk: -------------------------------------------------------------------------------- 1 | unknown var = uval 2 | 3 | all: 4 | test "$(subst a,b,value)" = "vblue" 5 | test "${subst a,b,va)lue}" = "vb)lue" 6 | test "$(subst /,\,ab/c)" = "ab\c" 7 | test '$(subst a,b,\\#)' = '\\#' 8 | test "$( subst a,b,value)" = "" 9 | test "$(Subst a,b,value)" = "" 10 | test "$(unknown var)" = "uval" 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/functions.mk: -------------------------------------------------------------------------------- 1 | all: 2 | test "$(subst e,EE,hello)" = "hEEllo" 3 | test "$(strip $(NULL) test data )" = "test data" 4 | test "$(findstring hell,hello)" = "hell" 5 | test "$(findstring heaven,hello)" = "" 6 | test "$(filter foo/%.c b%,foo/a.c b.c foo/a.o)" = "foo/a.c b.c" 7 | test "$(filter foo,foo bar)" = "foo" 8 | test "$(filter-out foo/%.c b%,foo/a.c b.c foo/a.o)" = "foo/a.o" 9 | test "$(filter-out %.c,foo,bar.c foo,bar.o)" = "foo,bar.o" 10 | test "$(sort .go a b aa A c cc)" = ".go A a aa b c cc" 11 | test "$(word 1, hello )" = "hello" 12 | test "$(word 2, hello )" = "" 13 | test "$(wordlist 1, 2, foo bar baz )" = "foo bar" 14 | test "$(words 1 2 3)" = "3" 15 | test "$(words )" = "0" 16 | test "$(firstword $(NULL) foo bar baz)" = "foo" 17 | test "$(firstword )" = "" 18 | test "$(dir foo.c path/foo.o dir/dir2/)" = "./ path/ dir/dir2/" 19 | test "$(notdir foo.c path/foo.o dir/dir2/)" = "foo.c foo.o " 20 | test "$(suffix src/foo.c dir/my.dir/foo foo.o)" = ".c .o" 21 | test "$(basename src/foo.c dir/my.dir/foo foo.c .c)" = "src/foo dir/my.dir/foo foo " 22 | test "$(addprefix src/,foo bar.c dir/foo)" = "src/foo src/bar.c src/dir/foo" 23 | test "$(addsuffix .c,foo dir/bar)" = "foo.c dir/bar.c" 24 | test "$(join a b c, 1 2 3)" = "a1 b2 c3" 25 | test "$(join a b, 1 2 3)" = "a1 b2 3" 26 | test "$(join a b c, 1 2)" = "a1 b2 c" 27 | test "$(if $(NULL) ,yes)" = "" 28 | test "$(if 1,yes,no)" = "yes" 29 | test "$(if ,yes,no )" = "no " 30 | test "$(if ,$(error Short-circuit problem))" = "" 31 | test "$(or $(NULL),1)" = "1" 32 | test "$(or $(NULL),2,$(warning TEST-FAIL bad or short-circuit))" = "2" 33 | test "$(and ,$(warning TEST-FAIL bad and short-circuit))" = "" 34 | test "$(and 1,2)" = "2" 35 | test "$(foreach i,foo bar,found:$(i))" = "found:foo found:bar" 36 | @echo TEST-PASS 37 | -------------------------------------------------------------------------------- /tests/functiontests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pymake.data 4 | import pymake.functions 5 | 6 | class VariableRefTest(unittest.TestCase): 7 | def test_get_expansions(self): 8 | e = pymake.data.StringExpansion('FOO', None) 9 | f = pymake.functions.VariableRef(None, e) 10 | 11 | exps = list(f.expansions()) 12 | self.assertEqual(len(exps), 1) 13 | 14 | class GetExpansionsTest(unittest.TestCase): 15 | def test_get_arguments(self): 16 | f = pymake.functions.SubstFunction(None) 17 | 18 | e1 = pymake.data.StringExpansion('FOO', None) 19 | e2 = pymake.data.StringExpansion('BAR', None) 20 | e3 = pymake.data.StringExpansion('BAZ', None) 21 | 22 | f.append(e1) 23 | f.append(e2) 24 | f.append(e3) 25 | 26 | exps = list(f.expansions()) 27 | self.assertEqual(len(exps), 3) 28 | 29 | def test_descend(self): 30 | f = pymake.functions.StripFunction(None) 31 | 32 | e = pymake.data.Expansion(None) 33 | 34 | e1 = pymake.data.StringExpansion('FOO', None) 35 | f1 = pymake.functions.VariableRef(None, e1) 36 | e.appendfunc(f1) 37 | 38 | f2 = pymake.functions.WildcardFunction(None) 39 | e2 = pymake.data.StringExpansion('foo/*', None) 40 | f2.append(e2) 41 | e.appendfunc(f2) 42 | 43 | f.append(e) 44 | 45 | exps = list(f.expansions()) 46 | self.assertEqual(len(exps), 1) 47 | 48 | exps = list(f.expansions(True)) 49 | self.assertEqual(len(exps), 3) 50 | 51 | self.assertFalse(f.is_filesystem_dependent) 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /tests/glob1.inc: -------------------------------------------------------------------------------- 1 | A=ok 2 | -------------------------------------------------------------------------------- /tests/glob2.inc: -------------------------------------------------------------------------------- 1 | B=ok -------------------------------------------------------------------------------- /tests/if-syntaxerr.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | ifeq ($(FOO,VAR)) 4 | all: 5 | @echo TEST_FAIL 6 | endif 7 | -------------------------------------------------------------------------------- /tests/ifdefs-nesting.mk: -------------------------------------------------------------------------------- 1 | ifdef RANDOM 2 | ifeq (,$(error Not evaluated!)) 3 | endif 4 | endif 5 | 6 | ifdef RANDOM 7 | ifeq (,) 8 | else ifeq (,$(error Not evaluated!)) 9 | endif 10 | endif 11 | 12 | all: 13 | @echo TEST-PASS 14 | -------------------------------------------------------------------------------- /tests/ifdefs.mk: -------------------------------------------------------------------------------- 1 | ifdef FOO 2 | $(error FOO is not defined!) 3 | endif 4 | 5 | FOO = foo 6 | FOOFOUND = false 7 | BARFOUND = false 8 | BAZFOUND = false 9 | 10 | ifdef FOO 11 | FOOFOUND = true 12 | else ifdef BAR 13 | BARFOUND = true 14 | else 15 | BAZFOUND = true 16 | endif 17 | 18 | BAR2 = bar2 19 | FOO2FOUND = false 20 | BAR2FOUND = false 21 | BAZ2FOUND = false 22 | 23 | ifdef FOO2 24 | FOO2FOUND = true 25 | else ifdef BAR2 26 | BAR2FOUND = true 27 | else 28 | BAZ2FOUND = true 29 | endif 30 | 31 | FOO3FOUND = false 32 | BAR3FOUND = false 33 | BAZ3FOUND = false 34 | 35 | ifdef FOO3 36 | FOO3FOUND = true 37 | else ifdef BAR3 38 | BAR3FOUND = true 39 | else 40 | BAZ3FOUND = true 41 | endif 42 | 43 | ifdef RANDOM 44 | CONTINUATION = \ 45 | else \ 46 | endif 47 | endif 48 | 49 | ifndef ASDFJK 50 | else 51 | $(error ASFDJK was not set) 52 | endif 53 | 54 | TESTSET = 55 | 56 | ifdef TESTSET 57 | $(error TESTSET was not set) 58 | endif 59 | 60 | TESTEMPTY = $(NULL) 61 | ifndef TESTEMPTY 62 | $(error TEST-FAIL TESTEMPTY was probably expanded!) 63 | endif 64 | 65 | # ifneq ( a,a) 66 | # $(error Arguments to ifeq should be stripped before evaluation) 67 | # endif 68 | 69 | XSPACE = x # trick 70 | 71 | ifneq ($(NULL),$(NULL)) 72 | $(error TEST-FAIL ifneq) 73 | endif 74 | 75 | ifneq (x , x) 76 | $(error argument-stripping1) 77 | endif 78 | 79 | ifeq ( x,x ) 80 | $(error argument-stripping2) 81 | endif 82 | 83 | ifneq ($(XSPACE), x ) 84 | $(error argument-stripping3) 85 | endif 86 | 87 | ifeq 'x ' ' x' 88 | $(error TEST-FAIL argument-stripping4) 89 | endif 90 | 91 | all: 92 | test $(FOOFOUND) = true # FOOFOUND 93 | test $(BARFOUND) = false # BARFOUND 94 | test $(BAZFOUND) = false # BAZFOUND 95 | test $(FOO2FOUND) = false # FOO2FOUND 96 | test $(BAR2FOUND) = true # BAR2FOUND 97 | test $(BAZ2FOUND) = false # BAZ2FOUND 98 | test $(FOO3FOUND) = false # FOO3FOUND 99 | test $(BAR3FOUND) = false # BAR3FOUND 100 | test $(BAZ3FOUND) = true # BAZ3FOUND 101 | ifneq ($(FOO),foo) 102 | echo TEST-FAIL 'FOO neq foo: "$(FOO)"' 103 | endif 104 | ifneq ($(FOO), foo) # Whitespace after the comma is stripped 105 | echo TEST-FAIL 'FOO plus whitespace' 106 | endif 107 | ifeq ($(FOO), foo ) # But not trailing whitespace 108 | echo TEST-FAIL 'FOO plus trailing whitespace' 109 | endif 110 | ifeq ( $(FOO),foo) # Not whitespace after the paren 111 | echo TEST-FAIL 'FOO with leading whitespace' 112 | endif 113 | ifeq ($(FOO),$(NULL) foo) # Nor whitespace after expansion 114 | echo TEST-FAIL 'FOO with embedded ws' 115 | endif 116 | ifeq ($(BAR2),bar) 117 | echo TEST-FAIL 'BAR2 eq bar' 118 | endif 119 | ifeq '$(BAR3FOUND)' 'false' 120 | echo BAR3FOUND is ok 121 | else 122 | echo TEST-FAIL BAR3FOUND is not ok 123 | endif 124 | ifndef FOO 125 | echo TEST-FAIL "foo not defined?" 126 | endif 127 | @echo TEST-PASS 128 | -------------------------------------------------------------------------------- /tests/ignore-error.mk: -------------------------------------------------------------------------------- 1 | all: 2 | -rm foo 3 | +-rm bar 4 | -+rm baz 5 | @-rm bah 6 | -@rm humbug 7 | +-@rm sincere 8 | +@-rm flattery 9 | @+-rm will 10 | @-+rm not 11 | -+@rm save 12 | -@+rm you 13 | @echo TEST-PASS 14 | -------------------------------------------------------------------------------- /tests/implicit-chain.mk: -------------------------------------------------------------------------------- 1 | all: test.prog 2 | test "$$(cat $<)" = "Program: Object: Source: test.source" 3 | @echo TEST-PASS 4 | 5 | %.prog: %.object 6 | printf "Program: %s" "$$(cat $<)" > $@ 7 | 8 | %.object: %.source 9 | printf "Object: %s" "$$(cat $<)" > $@ 10 | 11 | %.source: 12 | printf "Source: %s" $@ > $@ 13 | -------------------------------------------------------------------------------- /tests/implicit-dir.mk: -------------------------------------------------------------------------------- 1 | # Implicit rules have special instructions to deal with directories, so that a pattern rule which doesn't directly apply 2 | # may still be used. 3 | 4 | all: dir/host_test.otest 5 | 6 | host_%.otest: %.osource extra.file 7 | @echo making $@ from $< 8 | 9 | test.osource: 10 | @echo TEST-FAIL should have made dir/test.osource 11 | 12 | dir/test.osource: 13 | @echo TEST-PASS made the correct dependency 14 | 15 | extra.file: 16 | @echo building $@ 17 | -------------------------------------------------------------------------------- /tests/implicit-terminal.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # the %.object rule is "terminal". This means that additional implicit rules cannot be chained to it. 4 | 5 | all: test.prog 6 | test "$$(cat $<)" = "Program: Object: Source: test.source" 7 | @echo TEST-FAIL 8 | 9 | %.prog: %.object 10 | printf "Program: %s" "$$(cat $<)" > $@ 11 | 12 | %.object:: %.source 13 | printf "Object: %s" "$$(cat $<)" > $@ 14 | 15 | %.source: 16 | printf "Source: %s" $@ > $@ 17 | -------------------------------------------------------------------------------- /tests/implicitsubdir.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | mkdir foo; \ 3 | touch test.in \ 4 | ) 5 | 6 | all: foo/test.out 7 | @echo TEST-PASS 8 | 9 | foo/%.out: %.in 10 | cp $< $@ 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/include-dynamic.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | if ! test -f include-dynamic.inc; then \ 3 | echo "TESTVAR = oldval" > include-dynamic.inc; \ 4 | sleep 2; \ 5 | echo "TESTVAR = newval" > include-dynamic.inc.in; \ 6 | fi \ 7 | ) 8 | 9 | # before running the 'all' rule, we should be rebuilding include-dynamic.inc, 10 | # because there is a rule to do so 11 | 12 | all: 13 | test $(TESTVAR) = newval 14 | test "$(MAKE_RESTARTS)" = 1 15 | @echo TEST-PASS 16 | 17 | include-dynamic.inc: include-dynamic.inc.in 18 | test "$(MAKE_RESTARTS)" = "" 19 | cp $< $@ 20 | 21 | include include-dynamic.inc 22 | -------------------------------------------------------------------------------- /tests/include-file.inc: -------------------------------------------------------------------------------- 1 | INCLUDED = yes 2 | -------------------------------------------------------------------------------- /tests/include-glob.mk: -------------------------------------------------------------------------------- 1 | include $(TESTPATH)/glob*.inc 2 | 3 | ifeq ($A_$B,ok_ok) 4 | all: 5 | @echo TEST-PASS 6 | else 7 | all: 8 | @echo TEST-FAIL 9 | endif 10 | -------------------------------------------------------------------------------- /tests/include-missing.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # If an include file isn't present and doesn't have a rule to remake it, make 4 | # should fail. 5 | 6 | include notfound.mk 7 | 8 | all: 9 | @echo TEST-FAIL 10 | -------------------------------------------------------------------------------- /tests/include-notfound.mk: -------------------------------------------------------------------------------- 1 | ifdef __WIN32__ 2 | PS:=\\# 3 | else 4 | PS:=/ 5 | endif 6 | 7 | ifneq ($(strip $(MAKEFILE_LIST)),$(NATIVE_TESTPATH)$(PS)include-notfound.mk) 8 | $(error MAKEFILE_LIST incorrect: '$(MAKEFILE_LIST)' (expected '$(NATIVE_TESTPATH)$(PS)include-notfound.mk')) 9 | endif 10 | 11 | -include notfound.inc-dummy 12 | 13 | ifneq ($(strip $(MAKEFILE_LIST)),$(NATIVE_TESTPATH)$(PS)include-notfound.mk) 14 | $(error MAKEFILE_LIST incorrect: '$(MAKEFILE_LIST)' (expected '$(NATIVE_TESTPATH)$(PS)include-notfound.mk')) 15 | endif 16 | 17 | all: 18 | @echo TEST-PASS 19 | 20 | -------------------------------------------------------------------------------- /tests/include-optional-warning.mk: -------------------------------------------------------------------------------- 1 | -include TEST-FAIL.mk 2 | 3 | all: 4 | @echo TEST-PASS 5 | -------------------------------------------------------------------------------- /tests/include-regen.mk: -------------------------------------------------------------------------------- 1 | # avoid infinite loops by not remaking makefiles with 2 | # double-colon no-dependency rules 3 | # http://www.gnu.org/software/make/manual/make.html#Remaking-Makefiles 4 | -include notfound.mk 5 | 6 | all: 7 | @echo TEST-PASS 8 | 9 | notfound.mk:: 10 | @echo TEST-FAIL 11 | -------------------------------------------------------------------------------- /tests/include-regen2.mk: -------------------------------------------------------------------------------- 1 | # make should make makefiles that it has rules for if they are 2 | # included 3 | include test.mk 4 | 5 | all: 6 | test "$(X)" = "1" 7 | @echo "TEST-PASS" 8 | 9 | test.mk: 10 | @echo "X = 1" > $@ 11 | -------------------------------------------------------------------------------- /tests/include-regen3.mk: -------------------------------------------------------------------------------- 1 | # make should make makefiles that it has rules for if they are 2 | # included 3 | -include test.mk 4 | 5 | all: 6 | test "$(X)" = "1" 7 | @echo "TEST-PASS" 8 | 9 | test.mk: 10 | @echo "X = 1" > $@ 11 | -------------------------------------------------------------------------------- /tests/include-required-fails.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # Required include targets that fail should abort execution. 4 | 5 | include dummy.mk 6 | 7 | all: 8 | @echo TEST-FAIL 9 | 10 | dummy.mk: 11 | @echo "Evaluated dummy.mk" 12 | exit 1 13 | -------------------------------------------------------------------------------- /tests/include-test.mk: -------------------------------------------------------------------------------- 1 | $(shell echo "INCLUDED2 = yes" >local-include.inc) 2 | 3 | include $(TESTPATH)/include-file.inc local-include.inc 4 | 5 | all: 6 | test "$(INCLUDED)" = "yes" 7 | test "$(INCLUDED2)" = "yes" 8 | @echo TEST-PASS 9 | -------------------------------------------------------------------------------- /tests/includedeps-norebuild.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | $(shell \ 4 | touch filemissing; \ 5 | sleep 2; \ 6 | touch file1; \ 7 | ) 8 | 9 | all: file1 10 | @echo TEST-PASS 11 | 12 | includedeps $(TESTPATH)/includedeps.deps 13 | 14 | file1: 15 | @echo TEST-FAIL 16 | -------------------------------------------------------------------------------- /tests/includedeps-sideeffects.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | 4 | all: file1 filemissing 5 | @echo TEST-PASS 6 | 7 | includedeps $(TESTPATH)/includedeps.deps 8 | 9 | file: 10 | touch $@ 11 | -------------------------------------------------------------------------------- /tests/includedeps-stripdotslash.deps: -------------------------------------------------------------------------------- 1 | ./test: TEST-PASS 2 | -------------------------------------------------------------------------------- /tests/includedeps-stripdotslash.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | test: 4 | @echo $< 5 | 6 | includedeps $(TESTPATH)/includedeps-stripdotslash.deps 7 | 8 | TEST-PASS: 9 | -------------------------------------------------------------------------------- /tests/includedeps-variables.deps: -------------------------------------------------------------------------------- 1 | $(FILE)1: filemissing 2 | -------------------------------------------------------------------------------- /tests/includedeps-variables.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | FILE = includedeps-variables 4 | 5 | all: $(FILE)1 6 | 7 | includedeps $(TESTPATH)/includedeps-variables.deps 8 | 9 | filemissing: 10 | @echo TEST-PASS 11 | -------------------------------------------------------------------------------- /tests/includedeps.deps: -------------------------------------------------------------------------------- 1 | file1: filemissing 2 | -------------------------------------------------------------------------------- /tests/includedeps.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | all: file1 4 | @echo TEST-PASS 5 | 6 | includedeps $(TESTPATH)/includedeps.deps 7 | 8 | file1: 9 | touch $@ 10 | -------------------------------------------------------------------------------- /tests/info.mk: -------------------------------------------------------------------------------- 1 | #T grep-for: "info-printed\ninfo-nth" 2 | all: 3 | 4 | INFO = info-printed 5 | 6 | $(info $(INFO)) 7 | $(info $(subst second,nth,info-second)) 8 | $(info TEST-PASS) 9 | -------------------------------------------------------------------------------- /tests/justprint-native.mk: -------------------------------------------------------------------------------- 1 | ## $(TOUCH) and $(RM) are native commands in pymake. 2 | ## Test that pymake --just-print just prints them. 3 | 4 | ifndef TOUCH 5 | TOUCH = touch 6 | endif 7 | 8 | all: 9 | $(RM) justprint-native-file1.txt 10 | $(TOUCH) justprint-native-file2.txt 11 | $(MAKE) --just-print -f $(TESTPATH)/justprint-native.mk justprint_target > justprint.log 12 | # make --just-print shouldn't have actually done anything. 13 | test ! -f justprint-native-file1.txt 14 | test -f justprint-native-file2.txt 15 | # but it should have printed each command 16 | grep -q 'touch justprint-native-file1.txt' justprint.log 17 | grep -q 'rm -f justprint-native-file2.txt' justprint.log 18 | grep -q 'this string is "unlikely to appear in the log by chance"' justprint.log 19 | # tidy up 20 | $(RM) justprint-native-file2.txt 21 | @echo TEST-PASS 22 | 23 | justprint_target: 24 | $(TOUCH) justprint-native-file1.txt 25 | $(RM) justprint-native-file2.txt 26 | this string is "unlikely to appear in the log by chance" 27 | 28 | .PHONY: justprint_target 29 | -------------------------------------------------------------------------------- /tests/justprint.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-n'] 2 | 3 | all: 4 | false # without -n, we wouldn't get past this 5 | TEST-PASS # heh 6 | -------------------------------------------------------------------------------- /tests/keep-going-doublecolon.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-k'] 2 | #T returncode: 2 3 | #T grep-for: "TEST-PASS" 4 | 5 | all:: t1 6 | @echo TEST-FAIL "(t1)" 7 | 8 | all:: t2 9 | @echo TEST-PASS 10 | 11 | t1: 12 | @false 13 | 14 | t2: 15 | touch $@ 16 | 17 | -------------------------------------------------------------------------------- /tests/keep-going-parallel.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-k', '-j2'] 2 | #T returncode: 2 3 | #T grep-for: "TEST-PASS" 4 | 5 | all: t1 slow1 slow2 slow3 t2 6 | 7 | t2: 8 | @echo TEST-PASS 9 | 10 | slow%: 11 | sleep 1 12 | -------------------------------------------------------------------------------- /tests/keep-going.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-k'] 2 | #T returncode: 2 3 | #T grep-for: "TEST-PASS" 4 | 5 | all: t2 t3 6 | 7 | t1: 8 | @false 9 | 10 | t2: t1 11 | @echo TEST-FAIL 12 | 13 | t3: 14 | @echo TEST-PASS 15 | -------------------------------------------------------------------------------- /tests/line-continuations.mk: -------------------------------------------------------------------------------- 1 | VAR = val1 \ 2 | val2 3 | 4 | VAR2 = val1space\ 5 | val2 6 | 7 | VAR3 = val3 \\\ 8 | cont3 9 | 10 | all: otarget test.target 11 | test "$(VAR)" = "val1 val2 " 12 | test "$(VAR2)" = "val1space val2" 13 | test '$(VAR3)' = 'val3 \ cont3' 14 | test "hello \ 15 | world" = "hello world" 16 | test "hello" = \ 17 | "hello" 18 | @echo TEST-PASS 19 | 20 | otarget: ; test "hello\ 21 | world" = "helloworld" 22 | 23 | test.target: %.target: ; test "hello\ 24 | world" = "helloworld" 25 | -------------------------------------------------------------------------------- /tests/link-search.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | touch libfoo.so \ 3 | ) 4 | 5 | all: -lfoo 6 | test "$<" = "libfoo.so" 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/makeflags.mk: -------------------------------------------------------------------------------- 1 | #T environment: {'MAKEFLAGS': 'OVAR=oval'} 2 | 3 | all: 4 | test "$(OVAR)" = "oval" 5 | test "$$OVAR" = "oval" 6 | @echo TEST-PASS 7 | 8 | -------------------------------------------------------------------------------- /tests/matchany.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # we should fail to make foo.ooo from foo.ooo.test 4 | all: foo.ooo 5 | @echo TEST-FAIL 6 | 7 | %.ooo: 8 | 9 | # this match-anything pattern should not apply to %.ooo 10 | %: %.test 11 | cp $< $@ 12 | 13 | foo.ooo.test: 14 | touch $@ 15 | -------------------------------------------------------------------------------- /tests/matchany2.mk: -------------------------------------------------------------------------------- 1 | # we should succeed in making foo.ooo from foo.ooo.test 2 | all: foo.ooo 3 | @echo TEST-PASS 4 | 5 | %.ooo: %.ccc 6 | exit 1 7 | 8 | # this match-anything rule is terminal, and therefore applies 9 | %:: %.test 10 | cp $< $@ 11 | 12 | foo.ooo.test: 13 | touch $@ 14 | -------------------------------------------------------------------------------- /tests/matchany3.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | echo "target" > target.in; \ 3 | ) 4 | 5 | all: target 6 | test "$$(cat $^)" = "target" 7 | @echo TEST-PASS 8 | 9 | %: %.in 10 | cp $< $@ 11 | -------------------------------------------------------------------------------- /tests/mkdir-fail.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | all: 3 | mkdir newdir/subdir 4 | test ! -d newdir/subdir 5 | test ! -d newdir 6 | rm -r newdir 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/mkdir.mk: -------------------------------------------------------------------------------- 1 | MKDIR ?= mkdir 2 | 3 | all: 4 | $(MKDIR) newdir 5 | test -d newdir 6 | # subdir, parent exists 7 | $(MKDIR) newdir/subdir 8 | test -d newdir/subdir 9 | # -p, existing dir 10 | $(MKDIR) -p newdir 11 | # -p, existing subdir 12 | $(MKDIR) -p newdir/subdir 13 | # multiple subdirs, existing parent 14 | $(MKDIR) newdir/subdir1 newdir/subdir2 15 | test -d newdir/subdir1 -a -d newdir/subdir2 16 | rm -r newdir 17 | # -p, subdir, no existing parent 18 | $(MKDIR) -p newdir/subdir 19 | test -d newdir/subdir 20 | rm -r newdir 21 | # -p, multiple subdirs, no existing parent 22 | $(MKDIR) -p newdir/subdir1 newdir/subdir2 23 | test -d newdir/subdir1 -a -d newdir/subdir2 24 | # -p, multiple existing subdirs 25 | $(MKDIR) -p newdir/subdir1 newdir/subdir2 26 | rm -r newdir 27 | @echo TEST-PASS 28 | -------------------------------------------------------------------------------- /tests/multiple-rules-prerequisite-merge.mk: -------------------------------------------------------------------------------- 1 | # When a target is defined multiple times, the prerequisites should get 2 | # merged. 3 | 4 | default: foo bar baz 5 | 6 | foo: 7 | test "$<" = "foo.in1" 8 | @echo TEST-PASS 9 | 10 | foo: foo.in1 11 | 12 | bar: bar.in1 13 | test "$<" = "bar.in1" 14 | test "$^" = "bar.in1 bar.in2" 15 | @echo TEST-PASS 16 | 17 | bar: bar.in2 18 | 19 | baz: baz.in2 20 | baz: baz.in1 21 | test "$<" = "baz.in1" 22 | test "$^" = "baz.in1 baz.in2" 23 | @echo TEST-PASS 24 | 25 | foo.in1 bar.in1 bar.in2 baz.in1 baz.in2: 26 | -------------------------------------------------------------------------------- /tests/native-command-delay-load.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | # This test exists to verify that sys.path is adjusted during command 4 | # execution and that delay importing a module will work. 5 | 6 | CMD = %pycmd delayloadfn 7 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 8 | 9 | all: 10 | $(CMD) 11 | @echo TEST-PASS 12 | 13 | -------------------------------------------------------------------------------- /tests/native-command-raise.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | #T grep-for: "Exception: info-exception" 4 | 5 | CMD = %pycmd asplode_raise 6 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 7 | 8 | all: 9 | @$(CMD) info-exception 10 | -------------------------------------------------------------------------------- /tests/native-command-return-fail1.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | 4 | CMD = %pycmd asplode_return 5 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 6 | 7 | all: 8 | $(CMD) 1 9 | -------------------------------------------------------------------------------- /tests/native-command-return-fail2.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | 4 | CMD = %pycmd asplode_return 5 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 6 | 7 | all: 8 | $(CMD) not-an-integer 9 | -------------------------------------------------------------------------------- /tests/native-command-return.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | CMD = %pycmd asplode_return 4 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 5 | 6 | all: 7 | $(CMD) 0 8 | -$(CMD) 1 9 | $(CMD) None 10 | -$(CMD) not-an-integer 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/native-command-shell-glob.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | all: 3 | mkdir shell-glob-test 4 | touch shell-glob-test/foo.txt 5 | touch shell-glob-test/bar.txt 6 | touch shell-glob-test/a.foo 7 | touch shell-glob-test/b.foo 8 | $(RM) shell-glob-test/*.txt 9 | $(RM) shell-glob-test/?.foo 10 | rmdir shell-glob-test 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/native-command-sys-exit-fail1.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | 4 | CMD = %pycmd asplode 5 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 6 | 7 | all: 8 | $(CMD) 1 9 | -------------------------------------------------------------------------------- /tests/native-command-sys-exit-fail2.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T returncode: 2 3 | 4 | CMD = %pycmd asplode 5 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 6 | 7 | all: 8 | $(CMD) not-an-integer 9 | -------------------------------------------------------------------------------- /tests/native-command-sys-exit.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | CMD = %pycmd asplode 4 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 5 | 6 | all: 7 | $(CMD) 0 8 | -$(CMD) 1 9 | $(CMD) None 10 | -$(CMD) not-an-integer 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/native-environment.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | export EXPECTED := some data 3 | 4 | PYCOMMANDPATH = $(TESTPATH) 5 | 6 | all: 7 | %pycmd writeenvtofile results EXPECTED 8 | test "$$(cat results)" = "$(EXPECTED)" 9 | %pycmd writesubprocessenvtofile results EXPECTED 10 | test "$$(cat results)" = "$(EXPECTED)" 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/native-pycommandpath-sep.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | EXPECTED := some data 3 | 4 | # verify that we can load native command modules from 5 | # multiple directories in PYCOMMANDPATH separated by the native 6 | # path separator 7 | ifdef __WIN32__ 8 | PS:=; 9 | else 10 | PS:=: 11 | endif 12 | CMD = %pycmd writetofile 13 | CMD2 = %pymod writetofile 14 | PYCOMMANDPATH = $(TESTPATH)$(PS)$(TESTPATH)/subdir 15 | 16 | all: 17 | $(CMD) results $(EXPECTED) 18 | test "$$(cat results)" = "$(EXPECTED)" 19 | $(CMD2) results2 $(EXPECTED) 20 | test "$$(cat results2)" = "$(EXPECTED)" 21 | @echo TEST-PASS 22 | -------------------------------------------------------------------------------- /tests/native-pycommandpath.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | EXPECTED := some data 3 | 4 | # verify that we can load native command modules from 5 | # multiple space-separated directories in PYCOMMANDPATH 6 | CMD = %pycmd writetofile 7 | CMD2 = %pymod writetofile 8 | PYCOMMANDPATH = $(TESTPATH) $(TESTPATH)/subdir 9 | 10 | all: 11 | $(CMD) results $(EXPECTED) 12 | test "$$(cat results)" = "$(EXPECTED)" 13 | $(CMD2) results2 $(EXPECTED) 14 | test "$$(cat results2)" = "$(EXPECTED)" 15 | @echo TEST-PASS 16 | -------------------------------------------------------------------------------- /tests/native-simple.mk: -------------------------------------------------------------------------------- 1 | ifndef TOUCH 2 | TOUCH = touch 3 | endif 4 | 5 | all: testfile {testfile2} (testfile3) 6 | test -f testfile 7 | test -f {testfile2} 8 | test -f "(testfile3)" 9 | @echo TEST-PASS 10 | 11 | testfile {testfile2} (testfile3): 12 | $(TOUCH) "$@" 13 | -------------------------------------------------------------------------------- /tests/native-touch.mk: -------------------------------------------------------------------------------- 1 | TOUCH ?= touch 2 | 3 | foo: 4 | $(TOUCH) bar 5 | $(TOUCH) baz 6 | $(MAKE) -f $(TESTPATH)/native-touch.mk baz 7 | $(TOUCH) -t 198007040802 baz 8 | $(MAKE) -f $(TESTPATH)/native-touch.mk baz 9 | 10 | bar: 11 | $(TOUCH) $@ 12 | 13 | baz: bar 14 | echo TEST-PASS 15 | $(TOUCH) $@ 16 | -------------------------------------------------------------------------------- /tests/newlines.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | # Test that we handle \\\n properly 4 | 5 | all: dep1 dep2 dep3 6 | cat testfile 7 | test `cat testfile` = "data"; 8 | test "$$(cat results)" = "$(EXPECTED)"; 9 | @echo TEST-PASS 10 | 11 | # Test that something that still needs to go to the shell works 12 | testfile: 13 | printf "data" \ 14 | >>$@ 15 | 16 | dep1: testfile 17 | 18 | # Test that something that does not need to go to the shell works 19 | dep2: 20 | $(echo foo) \ 21 | $(echo bar) 22 | 23 | export EXPECTED := some data 24 | 25 | CMD = %pycmd writeenvtofile 26 | PYCOMMANDPATH = $(TESTPATH) 27 | 28 | dep3: 29 | $(CMD) \ 30 | results EXPECTED 31 | -------------------------------------------------------------------------------- /tests/no-remake.mk: -------------------------------------------------------------------------------- 1 | $(shell date >testfile) 2 | 3 | all: testfile 4 | @echo TEST-PASS 5 | 6 | testfile: 7 | @echo TEST-FAIL "We shouldn't have remade this!" 8 | -------------------------------------------------------------------------------- /tests/nosuchfile.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | all: 4 | reallythereisnosuchcommand 5 | -------------------------------------------------------------------------------- /tests/notargets.mk: -------------------------------------------------------------------------------- 1 | $(NULL): foo.c 2 | @echo TEST-FAIL 3 | 4 | all: 5 | @echo TEST-PASS 6 | -------------------------------------------------------------------------------- /tests/notparallel.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j3'] 2 | 3 | include $(TESTPATH)/serial-rule-execution.mk 4 | 5 | all:: 6 | $(MAKE) -f $(TESTPATH)/parallel-simple.mk 7 | 8 | .NOTPARALLEL: 9 | -------------------------------------------------------------------------------- /tests/oneline-command-continuations.mk: -------------------------------------------------------------------------------- 1 | all: test 2 | @echo TEST-PASS 3 | 4 | test: ; test "Hello \ 5 | world" = "Hello world" 6 | -------------------------------------------------------------------------------- /tests/override-propagate.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-w', 'OVAR=oval'] 2 | 3 | OVAR=mval 4 | 5 | all: vartest run-override 6 | $(MAKE) -f $(TESTPATH)/override-propagate.mk vartest 7 | @echo TEST-PASS 8 | 9 | CLINE := OVAR=oval TESTPATH=$(TESTPATH) NATIVE_TESTPATH=$(NATIVE_TESTPATH) 10 | ifdef __WIN32__ 11 | CLINE += __WIN32__=1 12 | endif 13 | 14 | SORTED_CLINE := $(subst \,\\,$(sort $(CLINE))) 15 | 16 | vartest: 17 | @echo MAKELEVEL: '$(MAKELEVEL)' 18 | test '$(value MAKEFLAGS)' = 'w -- $$(MAKEOVERRIDES)' 19 | test '$(origin MAKEFLAGS)' = 'file' 20 | test '$(value MAKEOVERRIDES)' = '$${-*-command-variables-*-}' 21 | test "$(sort $(MAKEOVERRIDES))" = "$(SORTED_CLINE)" 22 | test '$(origin MAKEOVERRIDES)' = 'environment' 23 | test '$(origin -*-command-variables-*-)' = 'automatic' 24 | test "$(origin OVAR)" = "command line" 25 | test "$(OVAR)" = "oval" 26 | 27 | run-override: MAKEOVERRIDES= 28 | run-override: 29 | test "$(OVAR)" = "oval" 30 | $(MAKE) -f $(TESTPATH)/override-propagate.mk otest 31 | 32 | otest: 33 | test '$(value MAKEFLAGS)' = 'w' 34 | test '$(value MAKEOVERRIDES)' = '$${-*-command-variables-*-}' 35 | test '$(MAKEOVERRIDES)' = '' 36 | test '$(origin -*-command-variables-*-)' = 'undefined' 37 | test "$(OVAR)" = "mval" 38 | -------------------------------------------------------------------------------- /tests/parallel-dep-resolution.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j3'] 2 | #T returncode: 2 3 | 4 | all: t1 t2 5 | 6 | t1: 7 | sleep 1 8 | touch t1 t2 9 | -------------------------------------------------------------------------------- /tests/parallel-dep-resolution2.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j3'] 2 | #T returncode: 2 3 | 4 | all:: 5 | sleep 1 6 | touch somefile 7 | 8 | all:: somefile 9 | @echo TEST-PASS 10 | -------------------------------------------------------------------------------- /tests/parallel-native.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j2'] 2 | 3 | # ensure that calling python commands doesn't block other targets 4 | ifndef SLEEP 5 | SLEEP := sleep 6 | endif 7 | 8 | PRINTF = printf "$@:0:" >>results 9 | EXPECTED = target2:0:target1:0: 10 | 11 | all:: target1 target2 12 | cat results 13 | test "$$(cat results)" = "$(EXPECTED)" 14 | @echo TEST-PASS 15 | 16 | target1: 17 | $(SLEEP) 0.1 18 | $(PRINTF) 19 | 20 | target2: 21 | $(PRINTF) 22 | -------------------------------------------------------------------------------- /tests/parallel-simple.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j2'] 2 | 3 | # CAUTION: this makefile is also used by serial-toparallel.mk 4 | 5 | define SLOWMAKE 6 | printf "$@:0:" >>results 7 | sleep 0.5 8 | printf "$@:1:" >>results 9 | sleep 0.5 10 | printf "$@:2:" >>results 11 | endef 12 | 13 | EXPECTED = target1:0:target2:0:target1:1:target2:1:target1:2:target2:2: 14 | 15 | all:: target1 target2 16 | cat results 17 | test "$$(cat results)" = "$(EXPECTED)" 18 | @echo TEST-PASS 19 | 20 | target1: 21 | $(SLOWMAKE) 22 | 23 | target2: 24 | sleep 0.1 25 | $(SLOWMAKE) 26 | 27 | .PHONY: all 28 | -------------------------------------------------------------------------------- /tests/parallel-submake.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j2'] 2 | 3 | # A submake shouldn't return control to the parent until it has actually finished doing everything. 4 | 5 | all: 6 | -$(MAKE) -f $(TESTPATH)/parallel-submake.mk subtarget 7 | cat results 8 | test "$$(cat results)" = "0123" 9 | @echo TEST-PASS 10 | 11 | subtarget: succeed-slowly fail-quickly 12 | 13 | succeed-slowly: 14 | printf 0 >>results; sleep 1; printf 1 >>results; sleep 1; printf 2 >>results; sleep 1; printf 3 >>results 15 | 16 | fail-quickly: 17 | exit 1 18 | -------------------------------------------------------------------------------- /tests/parallel-toserial.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j4'] 2 | 3 | # Test that -j1 in a submake has the proper effect. 4 | 5 | define SLOWCMD 6 | printf "$@:0:" >>$(RFILE) 7 | sleep 0.5 8 | printf "$@:1:" >>$(RFILE) 9 | endef 10 | 11 | all: p1 p2 12 | subtarget: s1 s2 13 | 14 | p1 p2: RFILE = presult 15 | s1 s2: RFILE = sresult 16 | 17 | p1 s1: 18 | $(SLOWCMD) 19 | 20 | p2 s2: 21 | sleep 0.1 22 | $(SLOWCMD) 23 | 24 | all: 25 | $(MAKE) -j1 -f $(TESTPATH)/parallel-toserial.mk subtarget 26 | printf "presult: %s\n" "$$(cat presult)" 27 | test "$$(cat presult)" = "p1:0:p2:0:p1:1:p2:1:" 28 | printf "sresult: %s\n" "$$(cat sresult)" 29 | test "$$(cat sresult)" = "s1:0:s1:1:s2:0:s2:1:" 30 | @echo TEST-PASS 31 | 32 | -------------------------------------------------------------------------------- /tests/parallel-waiting.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j2'] 2 | 3 | EXPECTED = target1:before:target2:1:target2:2:target2:3:target1:after 4 | 5 | all:: target1 target2 6 | cat results 7 | test "$$(cat results)" = "$(EXPECTED)" 8 | @echo TEST-PASS 9 | 10 | target1: 11 | printf "$@:before:" >>results 12 | sleep 4 13 | printf "$@:after" >>results 14 | 15 | target2: 16 | sleep 0.2 17 | printf "$@:1:" >>results 18 | sleep 0.1 19 | printf "$@:2:" >>results 20 | sleep 0.1 21 | printf "$@:3:" >>results 22 | -------------------------------------------------------------------------------- /tests/parentheses.mk: -------------------------------------------------------------------------------- 1 | all: 2 | @(echo TEST-PASS) 3 | -------------------------------------------------------------------------------- /tests/parsertests.py: -------------------------------------------------------------------------------- 1 | import pymake.data, pymake.parser, pymake.parserdata, pymake.functions 2 | import unittest 3 | import logging 4 | 5 | 6 | def multitest(cls): 7 | for name in cls.testdata.keys(): 8 | def m(self, name=name): 9 | return self.runSingle(*self.testdata[name]) 10 | 11 | setattr(cls, 'test_%s' % name, m) 12 | return cls 13 | 14 | class TestBase(unittest.TestCase): 15 | def assertEqual(self, a, b, msg=""): 16 | """Actually print the values which weren't equal, if things don't work out!""" 17 | unittest.TestCase.assertEqual(self, a, b, "%s got %r expected %r" % (msg, a, b)) 18 | 19 | class DataTest(TestBase): 20 | testdata = { 21 | 'oneline': 22 | ("He\tllo", "f", 1, 0, 23 | ((0, "f", 1, 0), (2, "f", 1, 2), (3, "f", 1, 4))), 24 | 'twoline': 25 | ("line1 \n\tl\tine2", "f", 1, 4, 26 | ((0, "f", 1, 4), (5, "f", 1, 9), (6, "f", 1, 10), (7, "f", 2, 0), (8, "f", 2, 4), (10, "f", 2, 8), (13, "f", 2, 11))), 27 | } 28 | 29 | def runSingle(self, data, filename, line, col, results): 30 | d = pymake.parser.Data(data, 0, len(data), pymake.parserdata.Location(filename, line, col)) 31 | for pos, file, lineno, col in results: 32 | loc = d.getloc(pos) 33 | self.assertEqual(loc.path, file, "data file offset %i" % pos) 34 | self.assertEqual(loc.line, lineno, "data line offset %i" % pos) 35 | self.assertEqual(loc.column, col, "data col offset %i" % pos) 36 | multitest(DataTest) 37 | 38 | class LineEnumeratorTest(TestBase): 39 | testdata = { 40 | 'simple': ( 41 | 'Hello, world', [ 42 | ('Hello, world', 1), 43 | ] 44 | ), 45 | 'multi': ( 46 | 'Hello\nhappy \n\nworld\n', [ 47 | ('Hello', 1), 48 | ('happy ', 2), 49 | ('', 3), 50 | ('world', 4), 51 | ('', 5), 52 | ] 53 | ), 54 | 'continuation': ( 55 | 'Hello, \\\n world\nJellybeans!', [ 56 | ('Hello, \\\n world', 1), 57 | ('Jellybeans!', 3), 58 | ] 59 | ), 60 | 'multislash': ( 61 | 'Hello, \\\\\n world', [ 62 | ('Hello, \\\\', 1), 63 | (' world', 2), 64 | ] 65 | ) 66 | } 67 | 68 | def runSingle(self, s, lines): 69 | gotlines = [(d.s[d.lstart:d.lend], d.loc.line) for d in pymake.parser.enumeratelines(s, 'path')] 70 | self.assertEqual(gotlines, lines) 71 | 72 | multitest(LineEnumeratorTest) 73 | 74 | class IterTest(TestBase): 75 | testdata = { 76 | 'plaindata': ( 77 | pymake.parser.iterdata, 78 | "plaindata # test\n", 79 | "plaindata # test\n" 80 | ), 81 | 'makecomment': ( 82 | pymake.parser.itermakefilechars, 83 | "VAR = val # comment", 84 | "VAR = val " 85 | ), 86 | 'makeescapedcomment': ( 87 | pymake.parser.itermakefilechars, 88 | "VAR = val \# escaped hash", 89 | "VAR = val # escaped hash" 90 | ), 91 | 'makeescapedslash': ( 92 | pymake.parser.itermakefilechars, 93 | "VAR = val\\\\", 94 | "VAR = val\\\\", 95 | ), 96 | 'makecontinuation': ( 97 | pymake.parser.itermakefilechars, 98 | "VAR = VAL \\\n continuation # comment \\\n continuation", 99 | "VAR = VAL continuation " 100 | ), 101 | 'makecontinuation2': ( 102 | pymake.parser.itermakefilechars, 103 | "VAR = VAL \\ \\\n continuation", 104 | "VAR = VAL \\ continuation" 105 | ), 106 | 'makeawful': ( 107 | pymake.parser.itermakefilechars, 108 | "VAR = VAL \\\\# comment\n", 109 | "VAR = VAL \\" 110 | ), 111 | 'command': ( 112 | pymake.parser.itercommandchars, 113 | "echo boo # comment", 114 | "echo boo # comment", 115 | ), 116 | 'commandcomment': ( 117 | pymake.parser.itercommandchars, 118 | "echo boo \# comment", 119 | "echo boo \# comment", 120 | ), 121 | 'commandcontinue': ( 122 | pymake.parser.itercommandchars, 123 | "echo boo # \\\n\t command 2", 124 | "echo boo # \\\n command 2" 125 | ), 126 | } 127 | 128 | def runSingle(self, ifunc, idata, expected): 129 | d = pymake.parser.Data.fromstring(idata, 'IterTest data') 130 | 131 | it = pymake.parser._alltokens.finditer(d.s, 0, d.lend) 132 | actual = ''.join( [c for c, t, o, oo in ifunc(d, 0, ('dummy-token',), it)] ) 133 | self.assertEqual(actual, expected) 134 | 135 | if ifunc == pymake.parser.itermakefilechars: 136 | print("testing %r" % expected) 137 | self.assertEqual(pymake.parser.flattenmakesyntax(d, 0), expected) 138 | 139 | multitest(IterTest) 140 | 141 | 142 | # 'define': ( 143 | # pymake.parser.iterdefinechars, 144 | # "endef", 145 | # "" 146 | # ), 147 | # 'definenesting': ( 148 | # pymake.parser.iterdefinechars, 149 | # """define BAR # comment 150 | #random text 151 | #endef not what you think! 152 | #endef # comment is ok\n""", 153 | # """define BAR # comment 154 | #random text 155 | #endef not what you think!""" 156 | # ), 157 | # 'defineescaped': ( 158 | # pymake.parser.iterdefinechars, 159 | # """value \\ 160 | #endef 161 | #endef\n""", 162 | # "value endef" 163 | # ), 164 | 165 | class MakeSyntaxTest(TestBase): 166 | # (string, startat, stopat, stopoffset, expansion 167 | testdata = { 168 | 'text': ('hello world', 0, (), None, ['hello world']), 169 | 'singlechar': ('hello $W', 0, (), None, 170 | ['hello ', 171 | {'type': 'VariableRef', 172 | '.vname': ['W']} 173 | ]), 174 | 'stopat': ('hello: world', 0, (':', '='), 6, ['hello']), 175 | 'funccall': ('h $(flavor FOO)', 0, (), None, 176 | ['h ', 177 | {'type': 'FlavorFunction', 178 | '[0]': ['FOO']} 179 | ]), 180 | 'escapedollar': ('hello$$world', 0, (), None, ['hello$world']), 181 | 'varref': ('echo $(VAR)', 0, (), None, 182 | ['echo ', 183 | {'type': 'VariableRef', 184 | '.vname': ['VAR']} 185 | ]), 186 | 'dynamicvarname': ('echo $($(VARNAME):.c=.o)', 0, (':',), None, 187 | ['echo ', 188 | {'type': 'SubstitutionRef', 189 | '.vname': [{'type': 'VariableRef', 190 | '.vname': ['VARNAME']} 191 | ], 192 | '.substfrom': ['.c'], 193 | '.substto': ['.o']} 194 | ]), 195 | 'substref': (' $(VAR:VAL) := $(VAL)', 0, (':=', '+=', '=', ':'), 15, 196 | [' ', 197 | {'type': 'VariableRef', 198 | '.vname': ['VAR:VAL']}, 199 | ' ']), 200 | 'vadsubstref': (' $(VAR:VAL) = $(VAL)', 15, (), None, 201 | [{'type': 'VariableRef', 202 | '.vname': ['VAL']}, 203 | ]), 204 | } 205 | 206 | def compareRecursive(self, actual, expected, path): 207 | self.assertEqual(len(actual), len(expected), 208 | "compareRecursive: %s %r" % (path, actual)) 209 | for i in range(0, len(actual)): 210 | ipath = path + [i] 211 | 212 | a, isfunc = actual[i] 213 | e = expected[i] 214 | if isinstance(e, str): 215 | self.assertEqual(a, e, "compareRecursive: %s" % (ipath,)) 216 | else: 217 | self.assertEqual(type(a), getattr(pymake.functions, e['type']), 218 | "compareRecursive: %s" % (ipath,)) 219 | for k, v in e.items(): 220 | if k == 'type': 221 | pass 222 | elif k[0] == '[': 223 | item = int(k[1:-1]) 224 | proppath = ipath + [item] 225 | self.compareRecursive(a[item], v, proppath) 226 | elif k[0] == '.': 227 | item = k[1:] 228 | proppath = ipath + [item] 229 | self.compareRecursive(getattr(a, item), v, proppath) 230 | else: 231 | raise Exception("Unexpected property at %s: %s" % (ipath, k)) 232 | 233 | def runSingle(self, s, startat, stopat, stopoffset, expansion): 234 | d = pymake.parser.Data.fromstring(s, pymake.parserdata.Location('testdata', 1, 0)) 235 | 236 | a, t, offset = pymake.parser.parsemakesyntax(d, startat, stopat, pymake.parser.itermakefilechars) 237 | self.compareRecursive(a, expansion, []) 238 | self.assertEqual(offset, stopoffset) 239 | 240 | multitest(MakeSyntaxTest) 241 | 242 | class VariableTest(TestBase): 243 | testdata = """ 244 | VAR = value 245 | VARNAME = TESTVAR 246 | $(VARNAME) = testvalue 247 | $(VARNAME:VAR=VAL) = moretesting 248 | IMM := $(VARNAME) # this is a comment 249 | MULTIVAR = val1 \\ 250 | val2 251 | VARNAME = newname 252 | """ 253 | expected = {'VAR': 'value', 254 | 'VARNAME': 'newname', 255 | 'TESTVAR': 'testvalue', 256 | 'TESTVAL': 'moretesting', 257 | 'IMM': 'TESTVAR ', 258 | 'MULTIVAR': 'val1 val2', 259 | 'UNDEF': None} 260 | 261 | def runTest(self): 262 | stmts = pymake.parser.parsestring(self.testdata, 'VariableTest') 263 | 264 | m = pymake.data.Makefile() 265 | stmts.execute(m) 266 | for k, v in self.expected.items(): 267 | flavor, source, val = m.variables.get(k) 268 | if val is None: 269 | self.assertEqual(val, v, 'variable named %s' % k) 270 | else: 271 | self.assertEqual(val.resolvestr(m, m.variables), v, 'variable named %s' % k) 272 | 273 | class SimpleRuleTest(TestBase): 274 | testdata = """ 275 | VAR = value 276 | TSPEC = dummy 277 | all: TSPEC = myrule 278 | all:: test test2 $(VAR) 279 | echo "Hello, $(TSPEC)" 280 | 281 | %.o: %.c 282 | $(CC) -o $@ $< 283 | """ 284 | 285 | def runTest(self): 286 | stmts = pymake.parser.parsestring(self.testdata, 'SimpleRuleTest') 287 | 288 | m = pymake.data.Makefile() 289 | stmts.execute(m) 290 | self.assertEqual(m.defaulttarget, 'all', "Default target") 291 | 292 | self.assertTrue(m.hastarget('all'), "Has 'all' target") 293 | target = m.gettarget('all') 294 | rules = target.rules 295 | self.assertEqual(len(rules), 1, "Number of rules") 296 | prereqs = rules[0].prerequisites 297 | self.assertEqual(prereqs, ['test', 'test2', 'value'], "Prerequisites") 298 | commands = rules[0].commands 299 | self.assertEqual(len(commands), 1, "Number of commands") 300 | expanded = commands[0].resolvestr(m, target.variables) 301 | self.assertEqual(expanded, 'echo "Hello, myrule"') 302 | 303 | irules = m.implicitrules 304 | self.assertEqual(len(irules), 1, "Number of implicit rules") 305 | 306 | irule = irules[0] 307 | self.assertEqual(len(irule.targetpatterns), 1, "%.o target pattern count") 308 | self.assertEqual(len(irule.prerequisites), 1, "%.o prerequisite count") 309 | self.assertEqual(irule.targetpatterns[0].match('foo.o'), 'foo', "%.o stem") 310 | 311 | if __name__ == '__main__': 312 | logging.basicConfig(level=logging.DEBUG) 313 | unittest.main() 314 | -------------------------------------------------------------------------------- /tests/path-length.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | 3 | $(shell \ 4 | mkdir foo; \ 5 | touch tfile; \ 6 | ) 7 | 8 | all: foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../foo/../tfile 9 | @echo TEST-PASS 10 | -------------------------------------------------------------------------------- /tests/pathdir/pathtest: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo Called shell script: 2f7cdd0b-7277-48c1-beaf-56cb0dbacb24 3 | -------------------------------------------------------------------------------- /tests/pathdir/pathtest.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/pymake/034ae9ea5b726e03647d049147c5dbf688e94aaf/tests/pathdir/pathtest.exe -------------------------------------------------------------------------------- /tests/pathdir/src/Makefile: -------------------------------------------------------------------------------- 1 | pathtest.exe: pathtest.cpp 2 | cl -EHsc -MT $^ 3 | -------------------------------------------------------------------------------- /tests/pathdir/src/pathtest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::printf("Called Windows executable: 2f7cdd0b-7277-48c1-beaf-56cb0dbacb24\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /tests/patsubst.mk: -------------------------------------------------------------------------------- 1 | all: 2 | test "$(patsubst foo,%.bar,foo)" = "%.bar" 3 | test "$(patsubst \%word,replace,word %word other)" = "word replace other" 4 | test "$(patsubst %.c,\%%.o,foo.c bar.o baz.cpp)" = "%foo.o bar.o baz.cpp" 5 | test "$(patsubst host_%.c,host_%.o,dir/host_foo.c host_bar.c)" = "dir/host_foo.c host_bar.o" 6 | test "$(patsubst foo,bar,dir/foo foo baz)" = "dir/foo bar baz" 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/phony.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | touch dep; \ 3 | sleep 2; \ 4 | touch all; \ 5 | ) 6 | 7 | all:: dep 8 | @echo TEST-PASS 9 | 10 | .PHONY: all 11 | -------------------------------------------------------------------------------- /tests/pycmd.py: -------------------------------------------------------------------------------- 1 | import os, sys, subprocess 2 | 3 | def writetofile(args): 4 | with open(args[0], 'w') as f: 5 | f.write(' '.join(args[1:])) 6 | 7 | def writeenvtofile(args): 8 | with open(args[0], 'w') as f: 9 | f.write(os.environ[args[1]]) 10 | 11 | def writesubprocessenvtofile(args): 12 | with open(args[0], 'w') as f: 13 | p = subprocess.Popen([sys.executable, "-c", 14 | "import os; print os.environ['%s']" % args[1]], 15 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 16 | stdout, stderr = p.communicate() 17 | assert p.returncode == 0 18 | f.write(stdout) 19 | 20 | def convertasplode(arg): 21 | try: 22 | return int(arg) 23 | except: 24 | return (None if arg == "None" else arg) 25 | 26 | def asplode(args): 27 | arg0 = convertasplode(args[0]) 28 | sys.exit(arg0) 29 | 30 | def asplode_return(args): 31 | arg0 = convertasplode(args[0]) 32 | return arg0 33 | 34 | def asplode_raise(args): 35 | raise Exception(args[0]) 36 | 37 | def delayloadfn(args): 38 | import delayload 39 | -------------------------------------------------------------------------------- /tests/recursive-set.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | FOO = $(FOO) 4 | 5 | all: 6 | echo $(FOO) 7 | @echo TEST-FAIL 8 | -------------------------------------------------------------------------------- /tests/recursive-set2.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | FOO = $(BAR) 4 | BAR = $(FOO) 5 | 6 | all: 7 | echo $(FOO) 8 | @echo TEST-FAIL 9 | -------------------------------------------------------------------------------- /tests/remake-mtime.mk: -------------------------------------------------------------------------------- 1 | # mtime(dep1) < mtime(target) so the target should not be made 2 | $(shell touch dep1; sleep 1; touch target) 3 | 4 | all: target 5 | echo TEST-PASS 6 | 7 | target: dep1 8 | echo TEST-FAIL target should not have been made 9 | 10 | dep1: dep2 11 | @echo "Remaking dep1 (actually not)" 12 | 13 | dep2: 14 | @echo "Making dep2 (actually not)" 15 | -------------------------------------------------------------------------------- /tests/rm-fail.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | all: 3 | mkdir newdir 4 | test -d newdir 5 | touch newdir/newfile 6 | $(RM) newdir 7 | @echo TEST-PASS 8 | -------------------------------------------------------------------------------- /tests/rm.mk: -------------------------------------------------------------------------------- 1 | all: 2 | # $(RM) defaults to -f 3 | $(RM) nosuchfile 4 | touch newfile 5 | test -f newfile 6 | $(RM) newfile 7 | test ! -f newfile 8 | mkdir newdir 9 | test -d newdir 10 | touch newdir/newfile 11 | mkdir newdir/subdir 12 | $(RM) -r newdir/subdir 13 | test ! -d newdir/subdir 14 | test -d newdir 15 | mkdir newdir/subdir1 newdir/subdir2 16 | $(RM) -r newdir/subdir1 newdir/subdir2 17 | test ! -d newdir/subdir1 -a ! -d newdir/subdir2 18 | test -d newdir 19 | $(RM) -r newdir 20 | test ! -d newdir 21 | @echo TEST-PASS 22 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Run the test(s) listed on the command line. If a directory is listed, the script will recursively 4 | walk the directory for files named .mk and run each. 5 | 6 | For each test, we run gmake -f test.mk. By default, make must exit with an exit code of 0, and must print 'TEST-PASS'. 7 | 8 | Each test is run in an empty directory. 9 | 10 | The test file may contain lines at the beginning to alter the default behavior. These are all evaluated as python: 11 | 12 | #T commandline: ['extra', 'params', 'here'] 13 | #T returncode: 2 14 | #T returncode-on: {'win32': 2} 15 | #T environment: {'VAR': 'VALUE} 16 | #T grep-for: "text" 17 | """ 18 | from __future__ import print_function 19 | 20 | from subprocess import Popen, PIPE, STDOUT 21 | from optparse import OptionParser 22 | import os, re, sys, shutil, glob 23 | 24 | class ParentDict(dict): 25 | def __init__(self, parent, **kwargs): 26 | self.d = dict(kwargs) 27 | self.parent = parent 28 | 29 | def __setitem__(self, k, v): 30 | self.d[k] = v 31 | 32 | def __getitem__(self, k): 33 | if k in self.d: 34 | return self.d[k] 35 | 36 | return self.parent[k] 37 | 38 | thisdir = os.path.dirname(os.path.abspath(__file__)) 39 | 40 | pymake = [sys.executable, os.path.join(os.path.dirname(thisdir), 'make.py')] 41 | manifest = os.path.join(thisdir, 'tests.manifest') 42 | 43 | o = OptionParser() 44 | o.add_option('-g', '--gmake', 45 | dest="gmake", default="gmake") 46 | o.add_option('-d', '--tempdir', 47 | dest="tempdir", default="_mktests") 48 | opts, args = o.parse_args() 49 | 50 | if len(args) == 0: 51 | args = [thisdir] 52 | 53 | makefiles = [] 54 | for a in args: 55 | if os.path.isfile(a): 56 | makefiles.append(a) 57 | elif os.path.isdir(a): 58 | makefiles.extend(sorted(glob.glob(os.path.join(a, '*.mk')))) 59 | 60 | def runTest(makefile, make, logfile, options): 61 | """ 62 | Given a makefile path, test it with a given `make` and return 63 | (pass, message). 64 | """ 65 | 66 | if os.path.exists(opts.tempdir): shutil.rmtree(opts.tempdir) 67 | os.mkdir(opts.tempdir, 0o755) 68 | 69 | logfd = open(logfile, 'w') 70 | p = Popen(make + options['commandline'], stdout=logfd, stderr=STDOUT, env=options['env']) 71 | logfd.close() 72 | retcode = p.wait() 73 | 74 | if retcode != options['returncode']: 75 | return False, "FAIL (returncode=%i)" % retcode 76 | 77 | logfd = open(logfile) 78 | stdout = logfd.read() 79 | logfd.close() 80 | 81 | if stdout.find('TEST-FAIL') != -1: 82 | print(stdout) 83 | return False, "FAIL (TEST-FAIL printed)" 84 | 85 | if options['grepfor'] and stdout.find(options['grepfor']) == -1: 86 | print(stdout) 87 | return False, "FAIL (%s not in output)" % options['grepfor'] 88 | 89 | if options['returncode'] == 0 and stdout.find('TEST-PASS') == -1: 90 | print(stdout) 91 | return False, 'FAIL (No TEST-PASS printed)' 92 | 93 | if options['returncode'] != 0: 94 | return True, 'PASS (retcode=%s)' % retcode 95 | 96 | return True, 'PASS' 97 | 98 | print("%-30s%-28s%-28s" % ("Test:", "gmake:", "pymake:")) 99 | 100 | gmakefails = 0 101 | pymakefails = 0 102 | 103 | tre = re.compile('^#T (gmake |pymake )?([a-z-]+)(?:: (.*))?$') 104 | 105 | for makefile in makefiles: 106 | # For some reason, MAKEFILE_LIST uses native paths in GNU make on Windows 107 | # (even in MSYS!) so we pass both TESTPATH and NATIVE_TESTPATH 108 | cline = ['-C', opts.tempdir, '-f', os.path.abspath(makefile), 'TESTPATH=%s' % thisdir.replace('\\','/'), 'NATIVE_TESTPATH=%s' % thisdir] 109 | if sys.platform == 'win32': 110 | #XXX: hack so we can specialize the separator character on windows. 111 | # we really shouldn't need this, but y'know 112 | cline += ['__WIN32__=1'] 113 | 114 | options = { 115 | 'returncode': 0, 116 | 'grepfor': None, 117 | 'env': dict(os.environ), 118 | 'commandline': cline, 119 | 'pass': True, 120 | 'skip': False, 121 | } 122 | 123 | gmakeoptions = ParentDict(options) 124 | pymakeoptions = ParentDict(options) 125 | 126 | dmap = {None: options, 'gmake ': gmakeoptions, 'pymake ': pymakeoptions} 127 | 128 | mdata = open(makefile) 129 | for line in mdata: 130 | line = line.strip() 131 | m = tre.search(line) 132 | if m is None: 133 | break 134 | 135 | make, key, data = m.group(1, 2, 3) 136 | d = dmap[make] 137 | if data is not None: 138 | data = eval(data) 139 | if key == 'commandline': 140 | assert make is None 141 | d['commandline'].extend(data) 142 | elif key == 'returncode': 143 | d['returncode'] = data 144 | elif key == 'returncode-on': 145 | if sys.platform in data: 146 | d['returncode'] = data[sys.platform] 147 | elif key == 'environment': 148 | for k, v in data.items(): 149 | d['env'][k] = v 150 | elif key == 'grep-for': 151 | d['grepfor'] = data 152 | elif key == 'fail': 153 | d['pass'] = False 154 | elif key == 'skip': 155 | d['skip'] = True 156 | else: 157 | print("%s: Unexpected #T key: %s" % (makefile, key), file=sys.stderr) 158 | sys.exit(1) 159 | 160 | mdata.close() 161 | 162 | if gmakeoptions['skip']: 163 | gmakepass, gmakemsg = True, '' 164 | else: 165 | gmakepass, gmakemsg = runTest(makefile, [opts.gmake], 166 | makefile + '.gmakelog', gmakeoptions) 167 | 168 | if gmakeoptions['pass']: 169 | if not gmakepass: 170 | gmakefails += 1 171 | else: 172 | if gmakepass: 173 | gmakefails += 1 174 | gmakemsg = "UNEXPECTED PASS" 175 | else: 176 | gmakemsg = "KNOWN FAIL" 177 | 178 | if pymakeoptions['skip']: 179 | pymakepass, pymakemsg = True, '' 180 | else: 181 | pymakepass, pymakemsg = runTest(makefile, pymake, 182 | makefile + '.pymakelog', pymakeoptions) 183 | 184 | if pymakeoptions['pass']: 185 | if not pymakepass: 186 | pymakefails += 1 187 | else: 188 | if pymakepass: 189 | pymakefails += 1 190 | pymakemsg = "UNEXPECTED PASS" 191 | else: 192 | pymakemsg = "OK (known fail)" 193 | 194 | print("%-30.30s%-28.28s%-28.28s" % (os.path.basename(makefile), 195 | gmakemsg, pymakemsg)) 196 | 197 | print() 198 | print("Summary:") 199 | print("%-30s%-28s%-28s" % ("", "gmake:", "pymake:")) 200 | 201 | if gmakefails == 0: 202 | gmakemsg = 'PASS' 203 | else: 204 | gmakemsg = 'FAIL (%i failures)' % gmakefails 205 | 206 | if pymakefails == 0: 207 | pymakemsg = 'PASS' 208 | else: 209 | pymakemsg = 'FAIL (%i failures)' % pymakefails 210 | 211 | print("%-30.30s%-28.28s%-28.28s" % ('', gmakemsg, pymakemsg)) 212 | 213 | shutil.rmtree(opts.tempdir) 214 | 215 | if gmakefails or pymakefails: 216 | sys.exit(1) 217 | -------------------------------------------------------------------------------- /tests/serial-dep-resolution.mk: -------------------------------------------------------------------------------- 1 | all: t1 t2 2 | @echo TEST-PASS 3 | 4 | t1: 5 | touch t1 t2 6 | -------------------------------------------------------------------------------- /tests/serial-doublecolon-execution.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['-j3'] 2 | 3 | # Commands of double-colon rules are always executed in order. 4 | 5 | all: dc 6 | cat status 7 | test "$$(cat status)" = "all1:all2:" 8 | @echo TEST-PASS 9 | 10 | dc:: slowt 11 | printf "all1:" >> status 12 | 13 | dc:: 14 | sleep 0.2 15 | printf "all2:" >> status 16 | 17 | slowt: 18 | sleep 1 19 | -------------------------------------------------------------------------------- /tests/serial-rule-execution.mk: -------------------------------------------------------------------------------- 1 | all:: 2 | touch somefile 3 | 4 | all:: somefile 5 | @echo TEST-PASS 6 | -------------------------------------------------------------------------------- /tests/serial-rule-execution2.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | # The dependencies of the command rule of a single-colon target are resolved before the rules without commands. 4 | 5 | all: export 6 | 7 | export: 8 | sleep 1 9 | touch somefile 10 | 11 | all: somefile 12 | test -f somefile 13 | @echo TEST-PASS 14 | -------------------------------------------------------------------------------- /tests/serial-toparallel.mk: -------------------------------------------------------------------------------- 1 | all:: 2 | $(MAKE) -j2 -f $(TESTPATH)/parallel-simple.mk 3 | 4 | all:: results 5 | @echo TEST-PASS 6 | -------------------------------------------------------------------------------- /tests/shellfunc.mk: -------------------------------------------------------------------------------- 1 | all: testfile 2 | test "$(shell cat $<)" = "Hello world" 3 | test "$(shell printf "\n")" = "" 4 | @echo TEST-PASS 5 | 6 | testfile: 7 | printf "Hello\nworld\n" > $@ 8 | -------------------------------------------------------------------------------- /tests/simple-makeflags.mk: -------------------------------------------------------------------------------- 1 | # There once was a time when MAKEFLAGS=w without any following spaces would 2 | # cause us to treat w as a target, not a flag. Silly! 3 | 4 | MAKEFLAGS=w 5 | 6 | all: 7 | $(MAKE) -f $(TESTPATH)/simple-makeflags.mk subt 8 | @echo TEST-PASS 9 | 10 | subt: 11 | -------------------------------------------------------------------------------- /tests/sort.mk: -------------------------------------------------------------------------------- 1 | # sort should remove duplicates 2 | all: 3 | @test "$(sort x a y b z c a z b x c y)" = "a b c x y z" 4 | @echo "TEST-PASS" 5 | -------------------------------------------------------------------------------- /tests/specified-target.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['VAR=all', '$(VAR)'] 2 | 3 | all: 4 | @echo TEST-FAIL: unexpected target 'all' 5 | 6 | $$(VAR): 7 | @echo TEST-PASS: expected target '$$(VAR)' 8 | -------------------------------------------------------------------------------- /tests/static-pattern.mk: -------------------------------------------------------------------------------- 1 | #T returncode: 2 2 | 3 | out/host_foo.o: host_%.o: host_%.c out 4 | cp $< $@ 5 | @echo TEST-FAIL 6 | -------------------------------------------------------------------------------- /tests/static-pattern2.mk: -------------------------------------------------------------------------------- 1 | all: foo.out 2 | test -f $^ 3 | @echo TEST-PASS 4 | 5 | foo.out: %.out: %.in 6 | test "$*" = "foo" 7 | cp $^ $@ 8 | 9 | foo.in: 10 | touch $@ 11 | -------------------------------------------------------------------------------- /tests/subdir/delayload.py: -------------------------------------------------------------------------------- 1 | # This module exists to test delay importing of modules at run-time. 2 | -------------------------------------------------------------------------------- /tests/subdir/pymod.py: -------------------------------------------------------------------------------- 1 | import testmodule 2 | 3 | def writetofile(args): 4 | with open(args[0], 'w') as f: 5 | f.write(' '.join(args[1:])) 6 | -------------------------------------------------------------------------------- /tests/subdir/testmodule.py: -------------------------------------------------------------------------------- 1 | # This is an empty module. It is imported by pymod.py to test that if a module 2 | # is loaded from the PYCOMMANDPATH, it can import other modules from the same 3 | # directory correctly. 4 | -------------------------------------------------------------------------------- /tests/submake-path.makefile2: -------------------------------------------------------------------------------- 1 | # -*- Mode: Makefile -*- 2 | 3 | shellresult := $(shell pathtest) 4 | ifneq (2f7cdd0b-7277-48c1-beaf-56cb0dbacb24,$(filter $(shellresult),2f7cdd0b-7277-48c1-beaf-56cb0dbacb24)) 5 | $(error pathtest not found in submake shell function) 6 | endif 7 | 8 | all: 9 | @pathtest 10 | @pathtest | grep -q 2f7cdd0b-7277-48c1-beaf-56cb0dbacb24 11 | @echo TEST-PASS 12 | -------------------------------------------------------------------------------- /tests/submake-path.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T grep-for: "2f7cdd0b-7277-48c1-beaf-56cb0dbacb24" 3 | 4 | ifdef __WIN32__ 5 | PS:=; 6 | else 7 | PS:=: 8 | endif 9 | 10 | export PATH := $(TESTPATH)/pathdir$(PS)$(PATH) 11 | 12 | # This is similar to subprocess-path.mk, except we also check $(shell) 13 | # invocations since they're affected by exported environment variables too, 14 | # but only in submakes! 15 | all: 16 | $(MAKE) -f $(TESTPATH)/submake-path.makefile2 17 | -------------------------------------------------------------------------------- /tests/submake.makefile2: -------------------------------------------------------------------------------- 1 | # -*- Mode: Makefile -*- 2 | 3 | $(info MAKEFLAGS = '$(MAKEFLAGS)') 4 | $(info MAKE = '$(MAKE)') 5 | $(info value MAKE = "$(value MAKE)") 6 | 7 | shellresult := $(shell echo -n $$EVAR) 8 | ifneq ($(shellresult),eval) 9 | $(error EVAR should be eval, is instead $(shellresult)) 10 | endif 11 | 12 | all: 13 | env 14 | test "$(MAKELEVEL)" = "1" 15 | echo "value(MAKE)" '$(value MAKE)' 16 | echo "value(MAKE_COMMAND)" = '$(value MAKE_COMMAND)' 17 | test "$(origin CVAR)" = "command line" 18 | test "$(CVAR)" = "c val=spac\ed" 19 | test "$(origin EVAR)" = "environment" 20 | test "$(EVAR)" = "eval" 21 | test "$(OVAL)" = "cline" 22 | test "$(OVAL2)" = "cline2" 23 | test "$(ALLVAR)" = "allspecific" 24 | @echo TEST-PASS 25 | -------------------------------------------------------------------------------- /tests/submake.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['CVAR=c val=spac\\ed', 'OVAL=cline', 'OVAL2=cline2'] 2 | 3 | export EVAR = eval 4 | override OVAL = makefile 5 | 6 | # exporting an override variable doesn't mean it's an override variable 7 | override OVAL2 = makefile2 8 | export OVAL2 9 | 10 | export ALLVAR 11 | ALLVAR = general 12 | all: ALLVAR = allspecific 13 | 14 | all: 15 | test "$(MAKELEVEL)" = "0" 16 | $(MAKE) -f $(TESTPATH)/submake.makefile2 17 | -------------------------------------------------------------------------------- /tests/subprocess-path.mk: -------------------------------------------------------------------------------- 1 | #T gmake skip 2 | #T grep-for: "2f7cdd0b-7277-48c1-beaf-56cb0dbacb24" 3 | 4 | ifdef __WIN32__ 5 | PS:=; 6 | else 7 | PS:=: 8 | endif 9 | 10 | export PATH := $(TESTPATH)/pathdir$(PS)$(PATH) 11 | 12 | # Test two commands. The first one shouldn't go through the shell and the 13 | # second one should. The pathdir subdirectory has a Windows executable called 14 | # pathtest.exe and a shell script called pathtest. We don't care which one is 15 | # run, just that one of the two is (we use a uuid + grep-for to make sure 16 | # that happens). 17 | # 18 | # FAQ: 19 | # Q. Why skip GNU Make? 20 | # A. Because $(TESTPATH) is a Windows-style path, and MSYS make doesn't take 21 | # too kindly to Windows paths in the PATH environment variable. 22 | # 23 | # Q. Why use an exe and not a batch file? 24 | # A. The use cases here were all exe files without the extension. Batch file 25 | # lookup has broken semantics if the .bat extension isn't passed. 26 | # 27 | # Q. Why are the commands silent? 28 | # A. So that we don't pass the grep-for test by mistake. 29 | all: 30 | @pathtest 31 | @pathtest | grep -q 2f7cdd0b-7277-48c1-beaf-56cb0dbacb24 32 | @echo TEST-PASS 33 | -------------------------------------------------------------------------------- /tests/tab-intro.mk: -------------------------------------------------------------------------------- 1 | # Initial tab characters should be treated well. 2 | 3 | THIS = a value 4 | 5 | ifdef THIS 6 | VAR = conditional value 7 | endif 8 | 9 | all: 10 | test "$(THIS)" = "another value" 11 | test "$(VAR)" = "conditional value" 12 | @echo TEST-PASS 13 | 14 | THAT = makefile syntax 15 | 16 | THIS = another value 17 | -------------------------------------------------------------------------------- /tests/target-specific.mk: -------------------------------------------------------------------------------- 1 | TESTVAR = anonval 2 | 3 | all: target.suffix target.suffix2 dummy host_test.py my.test1 my.test2 4 | @echo TEST-PASS 5 | 6 | target.suffix: TESTVAR = testval 7 | 8 | %.suffix: 9 | test "$(TESTVAR)" = "testval" 10 | 11 | %.suffix2: TESTVAR = testval2 12 | 13 | %.suffix2: 14 | test "$(TESTVAR)" = "testval2" 15 | 16 | %my: TESTVAR = dummyval 17 | 18 | dummy: 19 | test "$(TESTVAR)" = "dummyval" 20 | 21 | %.py: TESTVAR = pyval 22 | host_%.py: TESTVAR = hostval 23 | 24 | host_test.py: 25 | test "$(TESTVAR)" = "hostval" 26 | 27 | %.test1 %.test2: TESTVAR = %val 28 | 29 | my.test1 my.test2: 30 | test "$(TESTVAR)" = "%val" 31 | -------------------------------------------------------------------------------- /tests/unexport.mk: -------------------------------------------------------------------------------- 1 | #T environment: {'ENVVAR': 'envval'} 2 | 3 | VAR1 = val1 4 | VAR2 = val2 5 | VAR3 = val3 6 | 7 | unexport VAR3 8 | export VAR1 VAR2 VAR3 9 | unexport VAR2 ENVVAR 10 | unexport 11 | 12 | all: 13 | test "$(ENVVAR)" = "envval" # unexport.mk 14 | $(MAKE) -f $(TESTPATH)/unexport.submk 15 | @echo TEST-PASS 16 | -------------------------------------------------------------------------------- /tests/unexport.submk: -------------------------------------------------------------------------------- 1 | # -@- Mode: Makefile -@- 2 | 3 | unexport VAR1 4 | 5 | all: 6 | env 7 | test "$(VAR1)" = "val1" 8 | test "$(origin VAR1)" = "environment" 9 | test "$(VAR2)" = "" # VAR2 10 | test "$(VAR3)" = "val3" 11 | test "$(ENVVAR)" = "" 12 | $(MAKE) -f $(TESTPATH)/unexport.submk subt 13 | 14 | subt: 15 | test "$(VAR1)" = "" 16 | -------------------------------------------------------------------------------- /tests/unterminated-dollar.mk: -------------------------------------------------------------------------------- 1 | VAR = value$ 2 | VAR2 = other 3 | 4 | all: 5 | test "$(VAR)" = "value" 6 | @echo TEST-PASS 7 | -------------------------------------------------------------------------------- /tests/var-change-flavor.mk: -------------------------------------------------------------------------------- 1 | VAR = value1 2 | VAR := value2 3 | 4 | VAR2 := val1 5 | VAR2 = val2 6 | 7 | default: 8 | test "$(flavor VAR)" = "simple" 9 | test "$(VAR)" = "value2" 10 | test "$(flavor VAR2)" = "recursive" 11 | test "$(VAR2)" = "val2" 12 | @echo "TEST-PASS" 13 | -------------------------------------------------------------------------------- /tests/var-commandline.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['TESTVAR=$(MAKEVAL)', 'TESTVAR2:=$(MAKEVAL)'] 2 | 3 | MAKEVAL=testvalue 4 | 5 | all: 6 | test "$(TESTVAR)" = "testvalue" 7 | test "$(TESTVAR2)" = "" 8 | @echo "TEST-PASS" -------------------------------------------------------------------------------- /tests/var-overrides.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['CLINEVAR=clineval', 'CLINEVAR2=clineval2'] 2 | 3 | # this doesn't actually test overrides yet, because they aren't implemented in pymake, 4 | # but testing origins in general is important 5 | 6 | MVAR = mval 7 | CLINEVAR = deadbeef 8 | 9 | override CLINEVAR2 = mval2 10 | 11 | all: 12 | test "$(origin NOVAR)" = "undefined" 13 | test "$(CLINEVAR)" = "clineval" 14 | test "$(origin CLINEVAR)" = "command line" 15 | test "$(MVAR)" = "mval" 16 | test "$(origin MVAR)" = "file" 17 | test "$(@)" = "all" 18 | test "$(origin @)" = "automatic" 19 | test "$(origin CLINEVAR2)" = "override" 20 | test "$(CLINEVAR2)" = "mval2" 21 | @echo TEST-PASS 22 | -------------------------------------------------------------------------------- /tests/var-ref.mk: -------------------------------------------------------------------------------- 1 | VAR = value 2 | VAR2 == value 3 | 4 | VAR5 = $(NULL) $(NULL) 5 | VARC = value # comment 6 | 7 | $(VAR3) 8 | $(VAR4) 9 | $(VAR5) 10 | 11 | VAR6$(VAR5) = val6 12 | 13 | all: 14 | test "$( VAR)" = "" 15 | test "$(VAR2)" = "= value" 16 | test "${VAR2}" = "= value" 17 | test "$(VAR6 )" = "val6" 18 | test "$(VARC)" = "value " 19 | @echo TEST-PASS 20 | -------------------------------------------------------------------------------- /tests/var-set.mk: -------------------------------------------------------------------------------- 1 | #T commandline: ['OBASIC=oval'] 2 | 3 | BASIC = val 4 | 5 | TEST = $(TEST) 6 | 7 | TEST2 = $(TES 8 | TEST2 += T) 9 | 10 | TES T = val 11 | 12 | RECVAR = foo 13 | RECVAR += var baz 14 | 15 | IMMVAR := bloo 16 | IMMVAR += $(RECVAR) 17 | 18 | BASIC ?= notval 19 | 20 | all: BASIC = valall 21 | all: RECVAR += $(BASIC) 22 | all: IMMVAR += $(BASIC) 23 | all: UNSET += more 24 | all: OBASIC += allmore 25 | 26 | CHECKLIT = $(NULL) check 27 | all: CHECKLIT += appendliteral 28 | 29 | RECVAR = blimey 30 | 31 | TESTEMPTY = \ 32 | $(NULL) 33 | 34 | all: other 35 | test "$(TEST2)" = "val" 36 | test '$(value TEST2)' = '$$(TES T)' 37 | test "$(RECVAR)" = "blimey valall" 38 | test "$(IMMVAR)" = "bloo foo var baz valall" 39 | test "$(UNSET)" = "more" 40 | test "$(OBASIC)" = "oval" 41 | test "$(CHECKLIT)" = " check appendliteral" 42 | test "$(TESTEMPTY)" = "" 43 | @echo TEST-PASS 44 | 45 | OVAR = oval 46 | OVAR ?= onotval 47 | 48 | other: OVAR ?= ooval 49 | other: LATERVAR ?= lateroverride 50 | 51 | LATERVAR = olater 52 | 53 | other: 54 | test "$(OVAR)" = "oval" 55 | test "$(LATERVAR)" = "lateroverride" 56 | -------------------------------------------------------------------------------- /tests/var-substitutions.mk: -------------------------------------------------------------------------------- 1 | SIMPLEVAR = aabb.cc 2 | SIMPLEPERCENT = test_value%extra 3 | 4 | SIMPLE3SUBSTNAME = SIMPLEVAR:.dd 5 | $(SIMPLE3SUBSTNAME) = weirdval 6 | 7 | PERCENT = dummy 8 | 9 | SIMPLESUBST = $(SIMPLEVAR:.cc=.dd) 10 | SIMPLE2SUBST = $(SIMPLEVAR:.cc) 11 | SIMPLE3SUBST = $(SIMPLEVAR:.dd) 12 | SIMPLE4SUBST = $(SIMPLEVAR:.cc=.dd=.ee) 13 | SIMPLE5SUBST = $(SIMPLEVAR:.cc=%.dd) 14 | PERCENTSUBST = $(SIMPLEVAR:%.cc=%.ee) 15 | PERCENT2SUBST = $(SIMPLEVAR:aa%.cc=ff%.f) 16 | PERCENT3SUBST = $(SIMPLEVAR:aa%.dd=gg%.gg) 17 | PERCENT4SUBST = $(SIMPLEVAR:aa%.cc=gg) 18 | PERCENT5SUBST = $(SIMPLEVAR:aa) 19 | PERCENT6SUBST = $(SIMPLEVAR:%.cc=%.dd=%.ee) 20 | PERCENT7SUBST = $(SIMPLEVAR:$(PERCENT).cc=%.dd) 21 | PERCENT8SUBST = $(SIMPLEVAR:%.cc=$(PERCENT).dd) 22 | PERCENT9SUBST = $(SIMPLEVAR:$(PERCENT).cc=$(PERCENT).dd) 23 | PERCENT10SUBST = $(SIMPLEVAR:%%.bb.cc=zz.bb.cc) 24 | PERCENT11SUBST = $(SIMPLEPERCENT:test%value%extra=other%value%extra) 25 | 26 | SPACEDVAR = $(NULL) ex1.c ex2.c $(NULL) 27 | SPACEDSUBST = $(SPACEDVAR:.c=.o) 28 | 29 | all: 30 | test "$(SIMPLESUBST)" = "aabb.dd" 31 | test "$(SIMPLE2SUBST)" = "" 32 | test "$(SIMPLE3SUBST)" = "weirdval" 33 | test "$(SIMPLE4SUBST)" = "aabb.dd=.ee" 34 | test "$(SIMPLE5SUBST)" = "aabb%.dd" 35 | test "$(PERCENTSUBST)" = "aabb.ee" 36 | test "$(PERCENT2SUBST)" = "ffbb.f" 37 | test "$(PERCENT3SUBST)" = "aabb.cc" 38 | test "$(PERCENT4SUBST)" = "gg" 39 | test "$(PERCENT5SUBST)" = "" 40 | test "$(PERCENT6SUBST)" = "aabb.dd=%.ee" 41 | test "$(PERCENT7SUBST)" = "aabb.dd" 42 | test "$(PERCENT8SUBST)" = "aabb.dd" 43 | test "$(PERCENT9SUBST)" = "aabb.dd" 44 | test "$(PERCENT10SUBST)" = "aabb.cc" 45 | test "$(PERCENT11SUBST)" = "other_value%extra" 46 | test "$(SPACEDSUBST)" = "ex1.o ex2.o" 47 | @echo TEST-PASS 48 | 49 | PERCENT = % 50 | -------------------------------------------------------------------------------- /tests/vpath-directive-dynamic.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | mkdir subd1; \ 3 | touch subd1/test.in; \ 4 | ) 5 | 6 | VVAR = %.in subd1 7 | 8 | vpath $(VVAR) 9 | 10 | all: test.in 11 | test "$<" = "subd1/test.in" 12 | @echo TEST-PASS 13 | -------------------------------------------------------------------------------- /tests/vpath-directive.mk: -------------------------------------------------------------------------------- 1 | # On Windows, MSYS make takes Unix paths but Pymake takes Windows paths 2 | VPSEP := $(if $(and $(__WIN32__),$(.PYMAKE)),;,:) 3 | 4 | $(shell \ 5 | mkdir subd1 subd2 subd3; \ 6 | printf "reallybaddata" >subd1/foo.in; \ 7 | printf "gooddata" >subd2/foo.in; \ 8 | printf "baddata" >subd3/foo.in; \ 9 | touch subd1/foo.in2 subd2/foo.in2 subd3/foo.in2; \ 10 | ) 11 | 12 | vpath %.in subd 13 | 14 | vpath 15 | vpath %.in subd2$(VPSEP)subd3 16 | 17 | vpath %.in2 subd0 18 | vpath f%.in2 subd1 19 | vpath %.in2 $(VPSEP)subd2 20 | 21 | %.out: %.in 22 | test "$<" = "subd2/foo.in" 23 | cp $< $@ 24 | 25 | %.out2: %.in2 26 | test "$<" = "subd1/foo.in2" 27 | cp $< $@ 28 | 29 | all: foo.out foo.out2 30 | test "$$(cat foo.out)" = "gooddata" 31 | @echo TEST-PASS 32 | -------------------------------------------------------------------------------- /tests/vpath.mk: -------------------------------------------------------------------------------- 1 | VPATH = foo bar 2 | 3 | $(shell \ 4 | mkdir foo; touch foo/tfile1; \ 5 | mkdir bar; touch bar/tfile2 bar/tfile3 bar/test.objtest; \ 6 | sleep 2; \ 7 | touch bar/test.source; \ 8 | ) 9 | 10 | all: tfile1 tfile2 tfile3 test.objtest test.source 11 | test "$^" = "foo/tfile1 bar/tfile2 tfile3 test.objtest bar/test.source" 12 | @echo TEST-PASS 13 | 14 | tfile3: test.objtest 15 | 16 | %.objtest: %.source 17 | test "$<" = bar/test.source 18 | test "$@" = test.objtest 19 | -------------------------------------------------------------------------------- /tests/vpath2.mk: -------------------------------------------------------------------------------- 1 | VPATH = foo bar 2 | 3 | $(shell \ 4 | mkdir bar; touch bar/test.source; \ 5 | sleep 2; \ 6 | mkdir foo; touch foo/tfile1; \ 7 | touch bar/tfile2 bar/tfile3 bar/test.objtest; \ 8 | ) 9 | 10 | all: tfile1 tfile2 tfile3 test.objtest test.source 11 | test "$^" = "foo/tfile1 bar/tfile2 bar/tfile3 bar/test.objtest bar/test.source" 12 | @echo TEST-PASS 13 | 14 | tfile3: test.objtest 15 | 16 | %.objtest: %.source 17 | test "$<" = bar/test.source 18 | test "$@" = test.objtest 19 | -------------------------------------------------------------------------------- /tests/wildcards.mk: -------------------------------------------------------------------------------- 1 | $(shell \ 2 | mkdir foo; \ 3 | touch a.c b.c c.out foo/d.c; \ 4 | sleep 2; \ 5 | touch c.in; \ 6 | ) 7 | 8 | VPATH = foo 9 | 10 | all: c.out prog 11 | cat $< 12 | test "$$(cat $<)" = "remadec.out" 13 | @echo TEST-PASS 14 | 15 | *.out: %.out: %.in 16 | test "$@" = c.out 17 | test "$<" = c.in 18 | printf "remade$@" >$@ 19 | 20 | prog: *.c 21 | test "$^" = "a.c b.c" 22 | touch $@ 23 | -------------------------------------------------------------------------------- /tests/windows-paths.mk: -------------------------------------------------------------------------------- 1 | all: 2 | touch file.in 3 | printf "%s: %s\n\ttrue" '$(CURDIR)/file.out' '$(CURDIR)/file.in' >test.mk 4 | $(MAKE) -f test.mk $(CURDIR)/file.out 5 | @echo TEST-PASS 6 | --------------------------------------------------------------------------------