public inbox for isar-users@googlegroups.com
 help / color / mirror / Atom feed
From: Jan Kiszka <jan.kiszka@siemens.com>
To: Adriaan Schmidt <adriaan.schmidt@siemens.com>,
	isar-users@googlegroups.com
Subject: Re: [RFC PATCH 3/5] meta: add oe.utils
Date: Wed, 13 Oct 2021 12:17:57 +0200	[thread overview]
Message-ID: <a4049c00-47ca-5eba-df89-a9e040aa8aa8@siemens.com> (raw)
In-Reply-To: <20211012130413.1719424-4-adriaan.schmidt@siemens.com>

On 12.10.21 15:04, Adriaan Schmidt wrote:
> Taken unmodified from yocto-3.3.2 (commit 31c639eb8664059eb4ed711be9173c223b4cc940)

Reason missing ("will use X for Y").

Jan

> 
> Signed-off-by: Adriaan Schmidt <adriaan.schmidt@siemens.com>
> ---
>  meta/classes/base.bbclass |   2 +-
>  meta/lib/oe/utils.py      | 569 ++++++++++++++++++++++++++++++++++++++
>  2 files changed, 570 insertions(+), 1 deletion(-)
>  create mode 100644 meta/lib/oe/utils.py
> 
> diff --git a/meta/classes/base.bbclass b/meta/classes/base.bbclass
> index 13134ff..f42dcba 100644
> --- a/meta/classes/base.bbclass
> +++ b/meta/classes/base.bbclass
> @@ -21,7 +21,7 @@
>  THISDIR = "${@os.path.dirname(d.getVar('FILE', True))}"
>  FILESPATH = "${@base_set_filespath(["${FILE_DIRNAME}/${PF}", "${FILE_DIRNAME}/${P}", "${FILE_DIRNAME}/${PN}", "${FILE_DIRNAME}/files", "${FILE_DIRNAME}"], d)}"
>  
> -OE_IMPORTS += "os sys time oe.path oe.patch"
> +OE_IMPORTS += "os sys time oe.path oe.patch oe.utils"
>  OE_IMPORTS[type] = "list"
>  
>  def oe_import(d):
> diff --git a/meta/lib/oe/utils.py b/meta/lib/oe/utils.py
> new file mode 100644
> index 0000000..a84039f
> --- /dev/null
> +++ b/meta/lib/oe/utils.py
> @@ -0,0 +1,569 @@
> +#
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +
> +import subprocess
> +import multiprocessing
> +import traceback
> +
> +def read_file(filename):
> +    try:
> +        f = open( filename, "r" )
> +    except IOError as reason:
> +        return "" # WARNING: can't raise an error now because of the new RDEPENDS handling. This is a bit ugly. :M:
> +    else:
> +        data = f.read().strip()
> +        f.close()
> +        return data
> +    return None
> +
> +def ifelse(condition, iftrue = True, iffalse = False):
> +    if condition:
> +        return iftrue
> +    else:
> +        return iffalse
> +
> +def conditional(variable, checkvalue, truevalue, falsevalue, d):
> +    if d.getVar(variable) == checkvalue:
> +        return truevalue
> +    else:
> +        return falsevalue
> +
> +def vartrue(var, iftrue, iffalse, d):
> +    import oe.types
> +    if oe.types.boolean(d.getVar(var)):
> +        return iftrue
> +    else:
> +        return iffalse
> +
> +def less_or_equal(variable, checkvalue, truevalue, falsevalue, d):
> +    if float(d.getVar(variable)) <= float(checkvalue):
> +        return truevalue
> +    else:
> +        return falsevalue
> +
> +def version_less_or_equal(variable, checkvalue, truevalue, falsevalue, d):
> +    result = bb.utils.vercmp_string(d.getVar(variable), checkvalue)
> +    if result <= 0:
> +        return truevalue
> +    else:
> +        return falsevalue
> +
> +def both_contain(variable1, variable2, checkvalue, d):
> +    val1 = d.getVar(variable1)
> +    val2 = d.getVar(variable2)
> +    val1 = set(val1.split())
> +    val2 = set(val2.split())
> +    if isinstance(checkvalue, str):
> +        checkvalue = set(checkvalue.split())
> +    else:
> +        checkvalue = set(checkvalue)
> +    if checkvalue.issubset(val1) and checkvalue.issubset(val2):
> +        return " ".join(checkvalue)
> +    else:
> +        return ""
> +
> +def set_intersect(variable1, variable2, d):
> +    """
> +    Expand both variables, interpret them as lists of strings, and return the
> +    intersection as a flattened string.
> +
> +    For example:
> +    s1 = "a b c"
> +    s2 = "b c d"
> +    s3 = set_intersect(s1, s2)
> +    => s3 = "b c"
> +    """
> +    val1 = set(d.getVar(variable1).split())
> +    val2 = set(d.getVar(variable2).split())
> +    return " ".join(val1 & val2)
> +
> +def prune_suffix(var, suffixes, d):
> +    # See if var ends with any of the suffixes listed and
> +    # remove it if found
> +    for suffix in suffixes:
> +        if suffix and var.endswith(suffix):
> +            var = var[:-len(suffix)]
> +
> +    prefix = d.getVar("MLPREFIX")
> +    if prefix and var.startswith(prefix):
> +        var = var[len(prefix):]
> +
> +    return var
> +
> +def str_filter(f, str, d):
> +    from re import match
> +    return " ".join([x for x in str.split() if match(f, x, 0)])
> +
> +def str_filter_out(f, str, d):
> +    from re import match
> +    return " ".join([x for x in str.split() if not match(f, x, 0)])
> +
> +def build_depends_string(depends, task):
> +    """Append a taskname to a string of dependencies as used by the [depends] flag"""
> +    return " ".join(dep + ":" + task for dep in depends.split())
> +
> +def inherits(d, *classes):
> +    """Return True if the metadata inherits any of the specified classes"""
> +    return any(bb.data.inherits_class(cls, d) for cls in classes)
> +
> +def features_backfill(var,d):
> +    # This construct allows the addition of new features to variable specified
> +    # as var
> +    # Example for var = "DISTRO_FEATURES"
> +    # This construct allows the addition of new features to DISTRO_FEATURES
> +    # that if not present would disable existing functionality, without
> +    # disturbing distributions that have already set DISTRO_FEATURES.
> +    # Distributions wanting to elide a value in DISTRO_FEATURES_BACKFILL should
> +    # add the feature to DISTRO_FEATURES_BACKFILL_CONSIDERED
> +    features = (d.getVar(var) or "").split()
> +    backfill = (d.getVar(var+"_BACKFILL") or "").split()
> +    considered = (d.getVar(var+"_BACKFILL_CONSIDERED") or "").split()
> +
> +    addfeatures = []
> +    for feature in backfill:
> +        if feature not in features and feature not in considered:
> +            addfeatures.append(feature)
> +
> +    if addfeatures:
> +        d.appendVar(var, " " + " ".join(addfeatures))
> +
> +def all_distro_features(d, features, truevalue="1", falsevalue=""):
> +    """
> +    Returns truevalue if *all* given features are set in DISTRO_FEATURES,
> +    else falsevalue. The features can be given as single string or anything
> +    that can be turned into a set.
> +
> +    This is a shorter, more flexible version of
> +    bb.utils.contains("DISTRO_FEATURES", features, truevalue, falsevalue, d).
> +
> +    Without explicit true/false values it can be used directly where
> +    Python expects a boolean:
> +       if oe.utils.all_distro_features(d, "foo bar"):
> +           bb.fatal("foo and bar are mutually exclusive DISTRO_FEATURES")
> +
> +    With just a truevalue, it can be used to include files that are meant to be
> +    used only when requested via DISTRO_FEATURES:
> +       require ${@ oe.utils.all_distro_features(d, "foo bar", "foo-and-bar.inc")
> +    """
> +    return bb.utils.contains("DISTRO_FEATURES", features, truevalue, falsevalue, d)
> +
> +def any_distro_features(d, features, truevalue="1", falsevalue=""):
> +    """
> +    Returns truevalue if at least *one* of the given features is set in DISTRO_FEATURES,
> +    else falsevalue. The features can be given as single string or anything
> +    that can be turned into a set.
> +
> +    This is a shorter, more flexible version of
> +    bb.utils.contains_any("DISTRO_FEATURES", features, truevalue, falsevalue, d).
> +
> +    Without explicit true/false values it can be used directly where
> +    Python expects a boolean:
> +       if not oe.utils.any_distro_features(d, "foo bar"):
> +           bb.fatal("foo, bar or both must be set in DISTRO_FEATURES")
> +
> +    With just a truevalue, it can be used to include files that are meant to be
> +    used only when requested via DISTRO_FEATURES:
> +       require ${@ oe.utils.any_distro_features(d, "foo bar", "foo-or-bar.inc")
> +
> +    """
> +    return bb.utils.contains_any("DISTRO_FEATURES", features, truevalue, falsevalue, d)
> +
> +def parallel_make(d, makeinst=False):
> +    """
> +    Return the integer value for the number of parallel threads to use when
> +    building, scraped out of PARALLEL_MAKE. If no parallelization option is
> +    found, returns None
> +
> +    e.g. if PARALLEL_MAKE = "-j 10", this will return 10 as an integer.
> +    """
> +    if makeinst:
> +        pm = (d.getVar('PARALLEL_MAKEINST') or '').split()
> +    else:
> +        pm = (d.getVar('PARALLEL_MAKE') or '').split()
> +    # look for '-j' and throw other options (e.g. '-l') away
> +    while pm:
> +        opt = pm.pop(0)
> +        if opt == '-j':
> +            v = pm.pop(0)
> +        elif opt.startswith('-j'):
> +            v = opt[2:].strip()
> +        else:
> +            continue
> +
> +        return int(v)
> +
> +    return ''
> +
> +def parallel_make_argument(d, fmt, limit=None, makeinst=False):
> +    """
> +    Helper utility to construct a parallel make argument from the number of
> +    parallel threads specified in PARALLEL_MAKE.
> +
> +    Returns the input format string `fmt` where a single '%d' will be expanded
> +    with the number of parallel threads to use. If `limit` is specified, the
> +    number of parallel threads will be no larger than it. If no parallelization
> +    option is found in PARALLEL_MAKE, returns an empty string
> +
> +    e.g. if PARALLEL_MAKE = "-j 10", parallel_make_argument(d, "-n %d") will return
> +    "-n 10"
> +    """
> +    v = parallel_make(d, makeinst)
> +    if v:
> +        if limit:
> +            v = min(limit, v)
> +        return fmt % v
> +    return ''
> +
> +def packages_filter_out_system(d):
> +    """
> +    Return a list of packages from PACKAGES with the "system" packages such as
> +    PN-dbg PN-doc PN-locale-eb-gb removed.
> +    """
> +    pn = d.getVar('PN')
> +    blacklist = [pn + suffix for suffix in ('', '-dbg', '-dev', '-doc', '-locale', '-staticdev', '-src')]
> +    localepkg = pn + "-locale-"
> +    pkgs = []
> +
> +    for pkg in d.getVar('PACKAGES').split():
> +        if pkg not in blacklist and localepkg not in pkg:
> +            pkgs.append(pkg)
> +    return pkgs
> +
> +def getstatusoutput(cmd):
> +    return subprocess.getstatusoutput(cmd)
> +
> +
> +def trim_version(version, num_parts=2):
> +    """
> +    Return just the first <num_parts> of <version>, split by periods.  For
> +    example, trim_version("1.2.3", 2) will return "1.2".
> +    """
> +    if type(version) is not str:
> +        raise TypeError("Version should be a string")
> +    if num_parts < 1:
> +        raise ValueError("Cannot split to parts < 1")
> +
> +    parts = version.split(".")
> +    trimmed = ".".join(parts[:num_parts])
> +    return trimmed
> +
> +def cpu_count(at_least=1):
> +    cpus = len(os.sched_getaffinity(0))
> +    return max(cpus, at_least)
> +
> +def execute_pre_post_process(d, cmds):
> +    if cmds is None:
> +        return
> +
> +    for cmd in cmds.strip().split(';'):
> +        cmd = cmd.strip()
> +        if cmd != '':
> +            bb.note("Executing %s ..." % cmd)
> +            bb.build.exec_func(cmd, d)
> +
> +# For each item in items, call the function 'target' with item as the first 
> +# argument, extraargs as the other arguments and handle any exceptions in the
> +# parent thread
> +def multiprocess_launch(target, items, d, extraargs=None):
> +
> +    class ProcessLaunch(multiprocessing.Process):
> +        def __init__(self, *args, **kwargs):
> +            multiprocessing.Process.__init__(self, *args, **kwargs)
> +            self._pconn, self._cconn = multiprocessing.Pipe()
> +            self._exception = None
> +            self._result = None
> +
> +        def run(self):
> +            try:
> +                ret = self._target(*self._args, **self._kwargs)
> +                self._cconn.send((None, ret))
> +            except Exception as e:
> +                tb = traceback.format_exc()
> +                self._cconn.send((e, tb))
> +
> +        def update(self):
> +            if self._pconn.poll():
> +                (e, tb) = self._pconn.recv()
> +                if e is not None:
> +                    self._exception = (e, tb)
> +                else:
> +                    self._result = tb
> +
> +        @property
> +        def exception(self):
> +            self.update()
> +            return self._exception
> +
> +        @property
> +        def result(self):
> +            self.update()
> +            return self._result
> +
> +    max_process = int(d.getVar("BB_NUMBER_THREADS") or os.cpu_count() or 1)
> +    launched = []
> +    errors = []
> +    results = []
> +    items = list(items)
> +    while (items and not errors) or launched:
> +        if not errors and items and len(launched) < max_process:
> +            args = (items.pop(),)
> +            if extraargs is not None:
> +                args = args + extraargs
> +            p = ProcessLaunch(target=target, args=args)
> +            p.start()
> +            launched.append(p)
> +        for q in launched:
> +            # Have to manually call update() to avoid deadlocks. The pipe can be full and
> +            # transfer stalled until we try and read the results object but the subprocess won't exit
> +            # as it still has data to write (https://bugs.python.org/issue8426)
> +            q.update()
> +            # The finished processes are joined when calling is_alive()
> +            if not q.is_alive():
> +                if q.exception:
> +                    errors.append(q.exception)
> +                if q.result:
> +                    results.append(q.result)
> +                launched.remove(q)
> +    # Paranoia doesn't hurt
> +    for p in launched:
> +        p.join()
> +    if errors:
> +        msg = ""
> +        for (e, tb) in errors:
> +            if isinstance(e, subprocess.CalledProcessError) and e.output:
> +                msg = msg + str(e) + "\n"
> +                msg = msg + "Subprocess output:"
> +                msg = msg + e.output.decode("utf-8", errors="ignore")
> +            else:
> +                msg = msg + str(e) + ": " + str(tb) + "\n"
> +        bb.fatal("Fatal errors occurred in subprocesses:\n%s" % msg)
> +    return results
> +
> +def squashspaces(string):
> +    import re
> +    return re.sub(r"\s+", " ", string).strip()
> +
> +def format_pkg_list(pkg_dict, ret_format=None):
> +    output = []
> +
> +    if ret_format == "arch":
> +        for pkg in sorted(pkg_dict):
> +            output.append("%s %s" % (pkg, pkg_dict[pkg]["arch"]))
> +    elif ret_format == "file":
> +        for pkg in sorted(pkg_dict):
> +            output.append("%s %s %s" % (pkg, pkg_dict[pkg]["filename"], pkg_dict[pkg]["arch"]))
> +    elif ret_format == "ver":
> +        for pkg in sorted(pkg_dict):
> +            output.append("%s %s %s" % (pkg, pkg_dict[pkg]["arch"], pkg_dict[pkg]["ver"]))
> +    elif ret_format == "deps":
> +        for pkg in sorted(pkg_dict):
> +            for dep in pkg_dict[pkg]["deps"]:
> +                output.append("%s|%s" % (pkg, dep))
> +    else:
> +        for pkg in sorted(pkg_dict):
> +            output.append(pkg)
> +
> +    output_str = '\n'.join(output)
> +
> +    if output_str:
> +        # make sure last line is newline terminated
> +        output_str += '\n'
> +
> +    return output_str
> +
> +
> +# Helper function to get the host compiler version
> +# Do not assume the compiler is gcc
> +def get_host_compiler_version(d, taskcontextonly=False):
> +    import re, subprocess
> +
> +    if taskcontextonly and d.getVar('BB_WORKERCONTEXT') != '1':
> +        return
> +
> +    compiler = d.getVar("BUILD_CC")
> +    # Get rid of ccache since it is not present when parsing.
> +    if compiler.startswith('ccache '):
> +        compiler = compiler[7:]
> +    try:
> +        env = os.environ.copy()
> +        # datastore PATH does not contain session PATH as set by environment-setup-...
> +        # this breaks the install-buildtools use-case
> +        # env["PATH"] = d.getVar("PATH")
> +        output = subprocess.check_output("%s --version" % compiler, \
> +                    shell=True, env=env, stderr=subprocess.STDOUT).decode("utf-8")
> +    except subprocess.CalledProcessError as e:
> +        bb.fatal("Error running %s --version: %s" % (compiler, e.output.decode("utf-8")))
> +
> +    match = re.match(r".* (\d+\.\d+)\.\d+.*", output.split('\n')[0])
> +    if not match:
> +        bb.fatal("Can't get compiler version from %s --version output" % compiler)
> +
> +    version = match.group(1)
> +    return compiler, version
> +
> +
> +def host_gcc_version(d, taskcontextonly=False):
> +    import re, subprocess
> +
> +    if taskcontextonly and d.getVar('BB_WORKERCONTEXT') != '1':
> +        return
> +
> +    compiler = d.getVar("BUILD_CC")
> +    # Get rid of ccache since it is not present when parsing.
> +    if compiler.startswith('ccache '):
> +        compiler = compiler[7:]
> +    try:
> +        env = os.environ.copy()
> +        env["PATH"] = d.getVar("PATH")
> +        output = subprocess.check_output("%s --version" % compiler, \
> +                    shell=True, env=env, stderr=subprocess.STDOUT).decode("utf-8")
> +    except subprocess.CalledProcessError as e:
> +        bb.fatal("Error running %s --version: %s" % (compiler, e.output.decode("utf-8")))
> +
> +    match = re.match(r".* (\d+\.\d+)\.\d+.*", output.split('\n')[0])
> +    if not match:
> +        bb.fatal("Can't get compiler version from %s --version output" % compiler)
> +
> +    version = match.group(1)
> +    return "-%s" % version if version in ("4.8", "4.9") else ""
> +
> +
> +def get_multilib_datastore(variant, d):
> +    localdata = bb.data.createCopy(d)
> +    if variant:
> +        overrides = localdata.getVar("OVERRIDES", False) + ":virtclass-multilib-" + variant
> +        localdata.setVar("OVERRIDES", overrides)
> +        localdata.setVar("MLPREFIX", variant + "-")
> +    else:
> +        origdefault = localdata.getVar("DEFAULTTUNE_MULTILIB_ORIGINAL")
> +        if origdefault:
> +            localdata.setVar("DEFAULTTUNE", origdefault)
> +        overrides = localdata.getVar("OVERRIDES", False).split(":")
> +        overrides = ":".join([x for x in overrides if not x.startswith("virtclass-multilib-")])
> +        localdata.setVar("OVERRIDES", overrides)
> +        localdata.setVar("MLPREFIX", "")
> +    return localdata
> +
> +#
> +# Python 2.7 doesn't have threaded pools (just multiprocessing)
> +# so implement a version here
> +#
> +
> +from queue import Queue
> +from threading import Thread
> +
> +class ThreadedWorker(Thread):
> +    """Thread executing tasks from a given tasks queue"""
> +    def __init__(self, tasks, worker_init, worker_end):
> +        Thread.__init__(self)
> +        self.tasks = tasks
> +        self.daemon = True
> +
> +        self.worker_init = worker_init
> +        self.worker_end = worker_end
> +
> +    def run(self):
> +        from queue import Empty
> +
> +        if self.worker_init is not None:
> +            self.worker_init(self)
> +
> +        while True:
> +            try:
> +                func, args, kargs = self.tasks.get(block=False)
> +            except Empty:
> +                if self.worker_end is not None:
> +                    self.worker_end(self)
> +                break
> +
> +            try:
> +                func(self, *args, **kargs)
> +            except Exception as e:
> +                print(e)
> +            finally:
> +                self.tasks.task_done()
> +
> +class ThreadedPool:
> +    """Pool of threads consuming tasks from a queue"""
> +    def __init__(self, num_workers, num_tasks, worker_init=None,
> +            worker_end=None):
> +        self.tasks = Queue(num_tasks)
> +        self.workers = []
> +
> +        for _ in range(num_workers):
> +            worker = ThreadedWorker(self.tasks, worker_init, worker_end)
> +            self.workers.append(worker)
> +
> +    def start(self):
> +        for worker in self.workers:
> +            worker.start()
> +
> +    def add_task(self, func, *args, **kargs):
> +        """Add a task to the queue"""
> +        self.tasks.put((func, args, kargs))
> +
> +    def wait_completion(self):
> +        """Wait for completion of all the tasks in the queue"""
> +        self.tasks.join()
> +        for worker in self.workers:
> +            worker.join()
> +
> +def write_ld_so_conf(d):
> +    # Some utils like prelink may not have the correct target library paths
> +    # so write an ld.so.conf to help them
> +    ldsoconf = d.expand("${STAGING_DIR_TARGET}${sysconfdir}/ld.so.conf")
> +    if os.path.exists(ldsoconf):
> +        bb.utils.remove(ldsoconf)
> +    bb.utils.mkdirhier(os.path.dirname(ldsoconf))
> +    with open(ldsoconf, "w") as f:
> +        f.write(d.getVar("base_libdir") + '\n')
> +        f.write(d.getVar("libdir") + '\n')
> +
> +class ImageQAFailed(Exception):
> +    def __init__(self, description, name=None, logfile=None):
> +        self.description = description
> +        self.name = name
> +        self.logfile=logfile
> +
> +    def __str__(self):
> +        msg = 'Function failed: %s' % self.name
> +        if self.description:
> +            msg = msg + ' (%s)' % self.description
> +
> +        return msg
> +
> +def sh_quote(string):
> +    import shlex
> +    return shlex.quote(string)
> +
> +def directory_size(root, blocksize=4096):
> +    """
> +    Calculate the size of the directory, taking into account hard links,
> +    rounding up every size to multiples of the blocksize.
> +    """
> +    def roundup(size):
> +        """
> +        Round the size up to the nearest multiple of the block size.
> +        """
> +        import math
> +        return math.ceil(size / blocksize) * blocksize
> +
> +    def getsize(filename):
> +        """
> +        Get the size of the filename, not following symlinks, taking into
> +        account hard links.
> +        """
> +        stat = os.lstat(filename)
> +        if stat.st_ino not in inodes:
> +            inodes.add(stat.st_ino)
> +            return stat.st_size
> +        else:
> +            return 0
> +
> +    inodes = set()
> +    total = 0
> +    for root, dirs, files in os.walk(root):
> +        total += sum(roundup(getsize(os.path.join(root, name))) for name in files)
> +        total += roundup(getsize(root))
> +    return total
> 


-- 
Siemens AG, T RDA IOT
Corporate Competence Center Embedded Linux

  reply	other threads:[~2021-10-13 10:17 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-10-12 13:04 [RFC PATCH 0/5] Refactor mount logic Adriaan Schmidt
2021-10-12 13:04 ` [RFC PATCH 1/5] oe imports in central location Adriaan Schmidt
2021-10-12 13:04 ` [RFC PATCH 2/5] meta: refactor containerization Adriaan Schmidt
2021-10-13 10:17   ` Jan Kiszka
2021-10-12 13:04 ` [RFC PATCH 3/5] meta: add oe.utils Adriaan Schmidt
2021-10-13 10:17   ` Jan Kiszka [this message]
2021-10-20  6:49     ` Schmidt, Adriaan
2021-10-12 13:04 ` [RFC PATCH 4/5] meta: add mounts class Adriaan Schmidt
2021-10-13 10:31   ` Jan Kiszka
2021-10-20  7:02     ` Schmidt, Adriaan
2021-10-12 13:04 ` [RFC PATCH 5/5] meta: refactor to use the new mounting mechanism Adriaan Schmidt
2021-10-13 10:42   ` Jan Kiszka
2021-10-20  9:20     ` Schmidt, Adriaan
2021-11-22 14:45 ` [RFC PATCH 0/5] Refactor mount logic Anton Mikanovich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=a4049c00-47ca-5eba-df89-a9e040aa8aa8@siemens.com \
    --to=jan.kiszka@siemens.com \
    --cc=adriaan.schmidt@siemens.com \
    --cc=isar-users@googlegroups.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox