# This file is part of the open-lmake distribution (git@github.com:cesar-douady/open-lmake.git)
# Copyright (c) 2023-2026 Doliam
# This program is free software: you can redistribute/modify under the terms of the GPL-v3 (https://www.gnu.org/licenses/gpl-3.0.html).
# This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
'lmake.rules source file (as named in lmake.rules.__file__) is thoroughly commented, please refer to it.'
import sys as _sys
import os as _os
import os.path as _osp
import pwd as _pwd
import re as _re
import signal as _signal
import lmake
from . import autodeps,pdict
_lmake_lib = _osp.dirname(_osp.dirname(__file__))
class _RuleBase :
def __init_subclass__(cls) :
lmake._rules.append(cls) # list of rules
__special__ = None
# combined attributes are aggregated, combine is itself combined but not listed here as it is dealt with separately
# for dict, a value of None discards the entry, sequences are mapped to dict with value True and non sequence is like a singleton
# for set , a key of -<key> discards <key> , sequences are mapped to set and non sequence is like a singleton
# for list & tuple, entries are concatenated , sequences are mapped to list/tuple and non sequence is like a singleton
combine = {
'stems'
, 'targets' , 'side_targets'
, 'deps' , 'side_deps'
, 'environ' , 'environ_resources' , 'environ_ancillary'
, 'resources'
, 'views'
}
# atributes listed in paths must be part of a combined dict and values are concatenated :
# if a value contains '...' surrounded by separators, or at the beginning or end, these '...' are replaced by the inherited value
paths = {
'environ.PATH' : ':'
, 'environ.LD_LIBRARY_PATH' : ':'
, 'environ.MANPATH' : ':'
, 'environ.PYTHONPATH' : ':'
}
# name # must be specific for each rule, defaults to class name
# job_name # defaults to first target
stems = {} # defines stems as regexprs for use in targets & deps, e.g. {'File':r'.*'}
targets = {} # patterns used to trigger rule execution, refers to stems above through {} notation, e.g. {'OBJ':'{File}.o'}
# # in case of multiple matches, first matching entry is considered, others are ignored
# # targets may have flags (use - to reset), e.g. { 'TMP' : ('{File}.tmp/{Tmp*}','Incremental','-Essential') }, flags may be :
# # flag | default | description
# # -------------+-----------+--------------------------------------------------------------------------------------------
# # Essential | if static | show in graphic flow
# # Incremmental | | file is not unlinked before job execution and can be read before written to or unlinked
# # SourceOk | | no error if target is actually a source
# # NoUniquify | | dont uniquify file if incremental and file has several links to it
# # NoWarning | | dont warn if either uniquified or unlinked before job execution and file was generated by another job
# target # syntactic sugar for targets = {'<stdout>':<value>} (except that it is allowed)
side_targets = {} # patterns used to add flags based on pattern matching refering to stems above through {} notation, e.g. {'CACHE':'{File}.cache','incremental'}
side_deps = {} # .
class Rule(_RuleBase) :
# auto_mkdir = False # auto mkdir dir in case of chdir
# autodep = 'ld_audit' # autodep method : none, ld_audit, ld_preload, ld_preload_jemalloc, ptrace
# backend = 'local' # may be set anywhere in the inheritance hierarchy if execution must be remote
# check_abs_paths = False # check that absolute paths inside the repo are not stored in targets
# chroot_dir = '/' # chroot dir to execute cmd (if None, empty or absent, no chroot is not done)
# chroot_actions = () # actions to carry out when chroot to chroot_dir, a list/tuple of : 'user_name', 'resolve_conf'
# cache = None # cache used to store results for this rule. None means no caching
# cmd # runnable if set anywhere in the inheritance hierarchy (as shell str or python function), chained if several definitions
# compression = None # compression to use when caching :
# # - if None : no compression
# # - if a str : the compression method : zlib or zstd, level defaults to 1
# # - if a int : the compression level : a int between 0 (no compression) and 9 (best and slowest), method defaults zstd if supported, else zlib
# # - if a tuple : (method,level), as described above
# cwd = '' # cwd in which to run cmd. targets/deps are relative to it unless they start with /, in which case it means top root dir
# # defaults to the nearest root dir of the module in which the rule is defined
deps = {} # patterns used to express explicit depencies, full f-string notation with stems and targets defined, e.g. {'SRC':'{File}.c'}
# # deps may have flags (use - to reset), e.g. {'TOOL':('tool','Critical','-Essential')}, flags may be :
# # flag | default | description
# # ------------+---------+--------------------------------------------------------------------------------------------
# # Essential | x | show in graphic flow
# # Critical | | following deps are ignored if this dep is modified
# # IgnoreError | | accept dep even if generated in error
# dep # syntactic sugar for deps = {'<stdin>':<value>} (except that it is allowed)
# ete = 0 # Estimated Time Enroute, initial guess for job exec time (in s)
# force = False # if set, jobs are never up-to-date, they are rebuilt every time they are needed
# keep_tmp = False # keep tmp dir after job execution
# kill_daemons = False # ensure no process survive after job end
kill_sigs = () # signals to use to kill jobs (send them in turn followed by SIGKILL), 1 second apart, until job dies
# # 0's may be used to set a larger delay between 2 trials)
# lmake_root = '/my/installs/open-lmake' # absolute path of the open-lmake installation dir to be used by job (default is current installation dir)
# # dir is first searched in chroot_dir then in the native root
# lmake_view = '/lmake' # absolute path under which the open-lmake installation dir is seen (if None, empty, or absent, no bind mount is done)
max_retries_on_lost = 1 # max number of retries in case of job lost. 1 is a reasonable value
# max_runs = 0 # maximum number a job can be run in a single lmake command, unlimited if None or 0
max_stderr_len = 100 # maximum number of stderr lines shown in output (full content is accessible with lshow -e), 100 is a reasonable compromise
max_submits = 10 # maximum number a job can be submitted in a single lmake command, unlimited if None or 0
# prio = 0 # in case of ambiguity, rules are selected with highest prio first
python = ('$PYTHON',) # python used for callable cmd
# readdir_ok = False # if set, listing a local non-ignored dir is not an error
# repo_view = '/repo' # absolute path under which the root dir of the repo is seen (if None, empty, or absent, no bind mount is done)
shell = ('$SHELL',) # shell used for str cmd (_sh is usually /bin/sh which may test for dir existence before chdir, which defeats auto_mkdir)
start_delay = 3 # delay before sending a start message if job is not done by then, 3 is a reasonable compromise
# stderr_ok = False # if set, writing to stderr is not an error but a warning
# timeout = None # timeout allocated to job execution (in s), must be None or an int
# tmp_view = '/tmp' # view under which tmp dir is seen, may be :
# # - not specified, '' or None : do not mount tmp dir
# # - str : must be an absolute path which tmp dir is mounted on.
# # physical tmp dir is :
# # - a private sub-dir in $TMPDIR if provided in the environment
# # - else a private sub-dir in the LMAKE dir
# use_script = False # use a script to run job rather than calling interpreter with -c
environ = pdict( # job execution environment, handled as part of cmd (trigger rebuild upon modification)
HOME = '$REPO_ROOT' # favor repeatability by hiding use home dir some tools use at start up time
, PATH = '$LMAKE_ROOT/bin:$STD_PATH'
)
environ_resources = pdict() # job execution environment, handled as resources (trigger rebuild upon modification for jobs in error)
environ_ancillary = pdict( # job execution environment, does not trigger rebuild upon modification
UID = str(_os.getuid()) # this may be necessary by some tools and usually does not lead to user specific configuration
, USER = _pwd.getpwuid(_os.getuid()).pw_name # .
)
resources = { # used in conjunction with backend to inform it of the necessary resources to execute the job, same syntax as deps
'cpu' : 1 # number of cpu's to allocate to job
# , 'mem' : '100M' # memory to allocate to job
# , 'tmp' : '1G' # temporary disk space to allocate to job
} # follow the same syntax as deps
class AntiRule(_RuleBase) :
__special__ = 'anti' # AntiRule's are not executed, but defined at high enough prio, prevent other rules from being selected
prio = float('inf') # default to high prio as the goal of AntiRule's is to hide other rules
class SourceRule(_RuleBase) :
__special__ = 'generic_src'
prio = float('inf')
class HomelessRule(Rule) :
'base rule to redirect the HOME environment variable to tmp'
environ = pdict(HOME='$TMPDIR')
class TraceRule(Rule) :
'base rule to trace shell commands to stdout'
cmd = '''
exec 3>&1
export BASH_XTRACEFD=3
set -x
'''
class AliasRule(Rule) :
'base rule to make target an alias for deps'
force = True # force deps to always be built as they are guaranted available when job runs
side_targets = { '__PHONY__' : ('{*:.*}','phony') } # targets are useless and are typically not built
def cmd() :
print(lmake.depend( *deps.values() , verbose=True , read=True ))
cmd.shell = '''
# take care of managing awkward deps : single quote deps, replacing ' by '"'"' as ' protects against everything but '
ldepend -vR {' '.join("'"+v.replace("'","'"+'"'+"'"+'"'+"'")+"'" for v in deps.values())}
'''
class DirtyRule(Rule) :
side_targets = { '__NO_MATCH__' : ('{*:.*}','Incremental','NoWarning') }
class _PyRule(Rule) :
environ = pdict(PYTHONPATH='$LMAKE_ROOT/lib')
class Py2Rule(_PyRule) :
'base rule that handle pyc creation when importing modules in python'
# python reads the pyc file and compare stored date with actual py date (through a stat), but semantic is to read the py file
side_targets = { '__PYC__' : ( r'{*:(?:.+/)?}{*:\w+}.pyc' , 'ignore','top' ) }
python = ('$PYTHON2',)
environ = pdict( LD_LIBRARY_PATH='$PYTHON2_LD_LIBRARY_PATH' )
# this will be executed before cmd() of concrete subclasses as cmd() are chained in case of inheritance
def cmd() :
import sys
assert sys.version_info.major==2 , 'cannot use Py2Rule with python%d.%d'%(sys.version_info.major,sys.version_info.minor)
from lmake.import_machinery import fix_import
fix_import()
cmd.shell = '' # support shell cmd's that may launch python as a subprocess XXX! : manage to execute fix_import()
class Py3Rule(_PyRule) :
'base rule that handle pyc creation when importing modules in python'
# python reads the pyc file and compare stored date with actual py date (through a stat), but semantic is to read the py file (guaranteed if fix_import is called)
side_targets = {
'__PYC__' : ( r'{*:(?:.+/)?}__pycache__/{*:\w+}.{*:[\w.-]+}.pyc' , 'incremental','top' )
, '__PYC_TMP__' : ( r'{*:(?:.+/)?}__pycache__/{*:\w+}.{*:[\w.-]+}.pyc.{*:\d+}' , 'ignore' ,'top' ) # these are temporary files guaranteed unique by python
}
environ = pdict( LD_LIBRARY_PATH='$PYTHON_LD_LIBRARY_PATH' )
# this will be executed before cmd() of concrete subclasses as cmd() are chained in case of inheritance
def cmd() :
import sys
assert sys.version_info.major==3 , 'cannot use Py3Rule with python%d.%d'%(sys.version_info.major,sys.version_info.minor)
from lmake.import_machinery import fix_import
fix_import() # deps/targets to pyc files are ignored, so nothing to do
cmd.shell = '' # support shell cmd's that may launch python as a subprocess XXX! : manage to execute fix_import()
PyRule = Py3Rule
class RustRule(Rule) :
'base rule for use by any code written in Rust (including cargo and rustc that are written in rust)'
autodep = 'ld_preload' # rust use a dedicated loader that does not call auditing code when using ld_audit
if 'RUSTUP_HOME' in _os.environ :
environ = {
'RUSTUP_HOME' : _os.environ['RUSTUP_HOME'] # ensure var is passed to job
, 'PATH' : _osp.dirname(_os.environ['RUSTUP_HOME'])+'/.cargo/bin:...' # ... stands for inherited value
}
class DirRule(Rule) :
'''
Base rule to ensure the existence of a dir by generating a target within said dir.
The default marker is '...'.
Usage :
class MyDirRule(DirRule) : pass
or :
class MyDirRule(DirRule) : marker='my_marker'
Note : in case of conflict with other rules, you may have to adjust prio
Then to ensure that dir exists :
lmake.depend(dir+'/'+marker)
'''
virtual = True
marker = '...'
target = fr'{{Dir:.+}}/{marker}'
backend = 'local' # command is faster than any other backend overhead
cmd = ''