public inbox for isar-users@googlegroups.com
 help / color / mirror / Atom feed
From: Alexander Smirnov <asmirnov@ilbers.de>
To: Henning Schild <henning.schild@siemens.com>
Cc: isar-users@googlegroups.com
Subject: Re: [PATCH 1/6] meta-isar-bin: Enable caching of deb packages
Date: Tue, 29 Aug 2017 11:20:40 +0300	[thread overview]
Message-ID: <d2d53aaf-d3c0-267e-bd58-87b840525ab1@ilbers.de> (raw)
In-Reply-To: <20170829095147.02ff5bc6@md1em3qc>

On 08/29/2017 10:51 AM, Henning Schild wrote:
> Am Tue, 29 Aug 2017 09:40:46 +0300
> schrieb Alexander Smirnov <asmirnov@ilbers.de>:
> 
>> On 08/28/2017 06:18 PM, Henning Schild wrote:
>>> This patch is very big, doing a lot of things at a time.
>>
>> Ok, I'll think how it can be split.
>>
>>> The commit message on the other hand is not very verbose.
>>
>> Half of the patch is the comments that describe in details what
>> happens in the code, so please questions and proposals.
> 
> I will comment on that once the patch got split. In general readable
> good does not require comments. Comments add redundancy and if comments
> are required it is usually a sign that the code is too complicated.

I think it's a matter of taste, but Ok, I got your position.

> 
>>>
>>> creating the repo is one step
>>>    - i think using reprepro is a good idea
>>> moving do_populate to that repo is another one
>>>    - i think this task should go away and the repo should be given to
>>>      multistrap
>>
>> Ok
>>
>>> and there are probably more ways to split that into reviewable
>>> pieces
>>
>> Please questions and proposals.
> 
> Yes, for v2.

Ok.

> 
>>>
>>> So this review is not too verbose, we need smaller pieces.
>>>
>>> More inline
>>>
>>> Am Sun, 27 Aug 2017 18:13:34 +0300
>>> schrieb Alexander Smirnov <asmirnov@ilbers.de>:
>>>    
>>>> From: Frank Lenormand <lenormf@gmail.com>
>>>>
>>>> Adds the possibility to create an apt repository for newly built
>>>> packages and then use that repo for populating the target
>>>> filesystem.
>>>>
>>>> Signed-off-by: Frank Lenormand <lenormf@gmail.com>
>>>> Signed-off-by: Alexander Smirnov <asmirnov@ilbers.de>
>>>> ---
>>>>    meta-isar-bin/conf/layer.conf                      |  18 +++
>>>>    meta-isar-bin/files/distributions.in               |   3 +
>>>>    meta-isar/conf/bblayers.conf.sample                |   1 +
>>>>    .../images/files/debian-configscript.sh            |   8 ++
>>>>    .../images/files/raspbian-configscript.sh          |   8 ++
>>>>    meta-isar/recipes-core/images/isar-image-base.bb   |   8 +-
>>>>    meta/classes/dpkg.bbclass                          | 126
>>>> +++++++++++++++++++--
>>>> meta/classes/image.bbclass                         |  30 +++-- 8
>>>> files changed, 186 insertions(+), 16 deletions(-) create mode
>>>> 100644 meta-isar-bin/conf/layer.conf create mode 100644
>>>> meta-isar-bin/files/distributions.in
>>>>
>>>> diff --git a/meta-isar-bin/conf/layer.conf
>>>> b/meta-isar-bin/conf/layer.conf new file mode 100644
>>>> index 0000000..24534e1
>>>> --- /dev/null
>>>> +++ b/meta-isar-bin/conf/layer.conf
>>>> @@ -0,0 +1,18 @@
>>>> +# Enable package caching with '1'
>>>> +DEBCACHE_ENABLED ?= "0"
>>>> +
>>>> +# Codename of the repository created by the caching class
>>>> +DEBDISTRONAME = "isar"
>>>> +
>>>> +# Path to the caching repository
>>>> +DEBCACHEDIR ?= "${LAYERDIR}/apt"
>>>> +
>>>> +# Path to the mount point of the repository within the target
>>>> rootfs, during +# population
>>>> +DEBCACHEMNT ?= "/opt/cache/apt"
>>>> +
>>>> +# Path to the databases used by `reprepro`
>>>> +DEBDBDIR ?= "${LAYERDIR}/db"
>>>> +
>>>> +# Path to the configuration files templates used by `reprepro`
>>>> +DEBFILESDIR ?= "${LAYERDIR}/files"
>>>> diff --git a/meta-isar-bin/files/distributions.in
>>>> b/meta-isar-bin/files/distributions.in new file mode 100644
>>>> index 0000000..59f4429
>>>> --- /dev/null
>>>> +++ b/meta-isar-bin/files/distributions.in
>>>> @@ -0,0 +1,3 @@
>>>> +Codename: {DISTRO_NAME}
>>>> +Architectures: i386 armhf source
>>>> +Components: main
>>>> diff --git a/meta-isar/conf/bblayers.conf.sample
>>>> b/meta-isar/conf/bblayers.conf.sample index 80867e7..53a362b 100644
>>>> --- a/meta-isar/conf/bblayers.conf.sample
>>>> +++ b/meta-isar/conf/bblayers.conf.sample
>>>> @@ -8,6 +8,7 @@ BBFILES ?= ""
>>>>    BBLAYERS ?= " \
>>>>      ##ISARROOT##/meta \
>>>>      ##ISARROOT##/meta-isar \
>>>> +  ##ISARROOT##/meta-isar-bin \
>>>>      "
>>>>    BBLAYERS_NON_REMOVABLE ?= " \
>>>>      ##ISARROOT##/meta \
>>>> diff --git
>>>> a/meta-isar/recipes-core/images/files/debian-configscript.sh
>>>> b/meta-isar/recipes-core/images/files/debian-configscript.sh index
>>>> 4ac37d0..b05babb 100644 ---
>>>> a/meta-isar/recipes-core/images/files/debian-configscript.sh +++
>>>> b/meta-isar/recipes-core/images/files/debian-configscript.sh @@
>>>> -8,6 +8,8 @@ set -e readonly MACHINE_SERIAL="$1" readonly
>>>> BAUDRATE_TTY="$2" readonly ROOTFS_DEV="$3"
>>>> +readonly DEBCACHEMNT="$4"
>>>> +readonly DEBDISTRONAME="$5"
>>>>    
>>>>    cat >> /etc/default/locale << EOF
>>>>    LANG=en_US.UTF-8
>>>> @@ -83,3 +85,9 @@ fi
>>>>    if [ -x "$TARGET/sbin/init" -a -x "$TARGET/usr/sbin/policy-rc.d"
>>>> ]; then rm -f $TARGET/usr/sbin/policy-rc.d
>>>>    fi
>>>> +
>>>> +mkdir -p /etc/apt/sources.list.d/
>>>> +cat <<EOF >/etc/apt/sources.list.d/${DEBDISTRONAME}.list
>>>> +deb file:${DEBCACHEMNT}/ ${DEBDISTRONAME} main
>>>> +deb-src file:${DEBCACHEMNT}/ ${DEBDISTRONAME} main
>>>> +EOF
>>>> diff --git
>>>> a/meta-isar/recipes-core/images/files/raspbian-configscript.sh
>>>> b/meta-isar/recipes-core/images/files/raspbian-configscript.sh
>>>> index 2454481..f995f4d 100644 ---
>>>> a/meta-isar/recipes-core/images/files/raspbian-configscript.sh +++
>>>> b/meta-isar/recipes-core/images/files/raspbian-configscript.sh @@
>>>> -8,6 +8,8 @@ set -e readonly MACHINE_SERIAL="$1" readonly
>>>> BAUDRATE_TTY="$2" readonly ROOTFS_DEV="$3"
>>>> +readonly DEBCACHEMNT="$4"
>>>> +readonly DEBDISTRONAME="$5"
>>>>    
>>>>    cat >> /etc/default/locale << EOF
>>>>    LANG=en_US.UTF-8
>>>> @@ -89,3 +91,9 @@ KERNEL_IMAGE=`ls /boot | grep vmlinuz`
>>>>    cat > /boot/config.txt << EOF
>>>>    kernel=$KERNEL_IMAGE
>>>>    EOF
>>>> +
>>>> +mkdir -p /etc/apt/sources.list.d/
>>>> +cat <<EOF >/etc/apt/sources.list.d/${DEBDISTRONAME}.list
>>>> +deb file:${DEBCACHEMNT}/ ${DEBDISTRONAME} main
>>>> +deb-src file:${DEBCACHEMNT}/ ${DEBDISTRONAME} main
>>>> +EOF
>>>> diff --git a/meta-isar/recipes-core/images/isar-image-base.bb
>>>> b/meta-isar/recipes-core/images/isar-image-base.bb index
>>>> b679d97..85e6b27 100644 ---
>>>> a/meta-isar/recipes-core/images/isar-image-base.bb +++
>>>> b/meta-isar/recipes-core/images/isar-image-base.bb @@ -49,8 +49,12
>>>> @@ do_rootfs() { sudo multistrap -a ${DISTRO_ARCH} -d "${S}" -f
>>>> "${WORKDIR}/multistrap.conf" || true
>>>>        # Configure root filesystem
>>>> -    sudo chroot ${S} /configscript.sh ${MACHINE_SERIAL}
>>>> ${BAUDRATE_TTY} \
>>>> -        ${ROOTFS_DEV}
>>>> +    sudo chroot ${S} /configscript.sh \
>>>> +        ${MACHINE_SERIAL}             \
>>>> +        ${BAUDRATE_TTY}               \
>>>> +        ${ROOTFS_DEV}                 \
>>>> +        ${DEBCACHEMNT}                \
>>>> +        ${DEBDISTRONAME}
>>>>        sudo rm ${S}/configscript.sh
>>>>    }
>>>>    
>>>> diff --git a/meta/classes/dpkg.bbclass b/meta/classes/dpkg.bbclass
>>>> index 360a95c..b1e201d 100644
>>>> --- a/meta/classes/dpkg.bbclass
>>>> +++ b/meta/classes/dpkg.bbclass
>>>> @@ -1,5 +1,5 @@
>>>>    # This software is a part of ISAR.
>>>> -# Copyright (C) 2015-2016 ilbers GmbH
>>>> +# Copyright (C) 2015-2017 ilbers GmbH
>>>>    
>>>>    # Add dependency from buildchroot creation
>>>>    DEPENDS += "buildchroot"
>>>> @@ -55,12 +55,124 @@ do_build() {
>>>>        sudo chroot ${BUILDCHROOT_DIR} /build.sh ${PP}/${SRC_DIR}
>>>>    }
>>>>    
>>>> -
>>>>    # Install package to dedicated deploy directory
>>>> -do_install() {
>>>> -    install -m 644 ${BUILDROOT}/*.deb ${DEPLOY_DIR_DEB}/
>>>> +do_binary_deb_install() {
>>>> +    readonly DIR_CACHE="${DEBCACHEDIR}/${DISTRO}"
>>>> +    readonly DIR_DB="${DEBDBDIR}/${DISTRO}"
>>>> +
>>>> +    if [ "${DEBCACHE_ENABLED}" != "0" ]; then
>>>> +        # If `bitbake` is running for the first time, the cache
>>>> doesn't exist
>>>> +        # yet and needs to be configured using a `distributions`
>>>> file.
>>>> +        # A template stored in the layer directory is
>>>> pre-processed to
>>>> +        # generate the configuration file, which is then placed in
>>>> the
>>>> +        # appropriate directory.
>>>> +        if [ ! -e "${DIR_CACHE}/conf/distributions" ]; then
>>>> +            mkdir -p "${DIR_CACHE}/conf"
>>>> +            sed -e "s#{DISTRO_NAME}#${DEBDISTRONAME}#g" \
>>>> +                "${DEBFILESDIR}/distributions.in" \
>>>> +                > "${DIR_CACHE}/conf/distributions"
>>>> +        fi
>>>
>>> This step should be in the image and not in every package. I guess
>>> it has a race with multiple packages running this init routine.
>>
>> Ok.
>>
>>>    
>>>> +        # Add binary and source packages to the deb cache
>>>> +        # If the cache doesn't exist yet, it will be created using
>>>> the
>>>> +        # `distributions` file generated above.
>>>> +        ls -1 "${BUILDROOT}"/*.deb "${BUILDROOT}"/*.dsc | while
>>>> read -r p; do
>>>> +            reprepro --waitforlock 3 -b "${DIR_CACHE}" --dbdir
>>>> "${DIR_DB}" \
>>>> +                -C main "include${p##*.}" "${DEBDISTRONAME}"
>>>> "${p}"
>>>> +        done
>>>> +    else
>>>> +        # Deb caching is disabled, simply copy all binary packages
>>>> to the
>>>> +        # deploy directory
>>>> +        mkdir -p "${DEPLOY_DIR_DEB}"
>>>> +        install -m 644 "${BUILDROOT}"/*.deb "${DEPLOY_DIR_DEB}/"
>>>> +    fi
>>>>    }
>>>>    
>>>> -addtask install after do_build
>>>> -do_install[dirs] = "${DEPLOY_DIR_DEB}"
>>>> -do_install[stamp-extra-info] = "${MACHINE}"
>>>> +addtask binary_deb_install after do_build
>>>> +do_binary_deb_install[stamp-extra-info] =
>>>> "${DISTRO}-${DISTRO_ARCH}"
>>>
>>> Here a task got renamed. This should be another patch ... one that i
>>> already tried to get in.
>>>    
>>
>> This implemented in the next patch (to keep this patch in original
>> format), but yes, it should be done here.
>>
>>>> +
>>>> +# Deb caching lambda run during the parsing phase that checks
>>>> whether the +# current package has to be rebuilt, or taken from the
>>>> cache +python __anonymous () {
>>>> +    if d.getVar("DEBCACHE_ENABLED", True) == "0":
>>>> +        # Deb caching is disabled, do nothing
>>>> +        return True
>>>> +
>>>> +    PN = d.getVar("PN", True)
>>>> +    PV = d.getVar("PV", True)
>>>> +    DISTRO_ARCH = d.getVar("DISTRO_ARCH", True)
>>>> +    DEBCACHEDIR = d.getVar("DEBCACHEDIR", True)
>>>> +    DEBDISTRONAME = d.getVar("DEBDISTRONAME", True)
>>>> +    DEBDBDIR = d.getVar("DEBDBDIR", True)
>>>> +    DISTRO = d.getVar("DISTRO", True)
>>>> +    path_cache = os.path.join(DEBCACHEDIR, DISTRO)
>>>> +    path_databases = os.path.join(DEBDBDIR, DISTRO)
>>>> +    path_distributions = os.path.join(path_cache, "conf",
>>>> "distributions") +
>>>> +    # The distributions file is needed by `reprepro` to know what
>>>> types
>>>> +    # of packages are supported, what the distribution name is,
>>>> etc.
>>>> +    # If it doesn't exist, we have nothing in the cache, do
>>>> nothing.
>>>> +    if not os.path.exists(path_distributions):
>>>> +        return
>>>> +
>>>> +    # Anonymous functions are run several times under different
>>>> contexts
>>>> +    # during the parsing phase, which would let the code that
>>>> follows be run
>>>> +    # as many times for the same package.
>>>> +    # In order to guarantee that our subroutine only runs once per
>>>> package, we
>>>> +    # use bitbake's "persist" API in order to have reliable
>>>> persistent storage
>>>> +    # accross calls of the lambda (using a simple variable in the
>>>> class won't
>>>> +    # work, as several contexts won't allow fetching its value).
>>>> +    pd = bb.persist_data.persist("DEBCACHE_PACKAGES", d)
>>>> +    if PN in pd and pd[PN] == PV:
>>>> +        return
>>>> +
>>>> +    import subprocess
>>>> +    try:
>>>> +        # The databases used by `reprepro` are not stored within
>>>> the cache in
>>>> +        # order to make versioning of only the files needed to use
>>>> the cache
>>>> +        # as an official Debian repository simpler.
>>>> +        # As such, if a developer uses a peer's cache to speed up
>>>> their build
>>>> +        # time but have never run bitbake, the database will not
>>>> have been
>>>> +        # created, so we regenerate them here.
>>>> +        if not os.path.exists(path_databases) and
>>>> os.path.exists(path_cache):
>>>> +            bb.note("Regenerating the cache databases...")
>>>> +            subprocess.check_call([
>>>> +                "reprepro",
>>>> +                "--waitforlock", "3",
>>>> +                "-b", path_cache,
>>>> +                "--dbdir", path_databases,
>>>> +                "export", DEBDISTRONAME,
>>>> +            ])
>>>> +
>>>> +        # Get a list of the versions of all the packages named
>>>> after the
>>>> +        # current bitbake package, and check whether the current
>>>> package
>>>> +        # version is returned.
>>>> +        # As `reprepro` always returns zero with this particular
>>>> operation, we
>>>> +        # have to use this workaround to check for a package in
>>>> the cache.
>>>> +        package_version = subprocess.check_output([
>>>> +            "reprepro",
>>>> +            "--waitforlock", "3",
>>>> +            "-b", path_cache,
>>>> +            "--dbdir", path_databases,
>>>> +            "-C", "main",
>>>> +            "-A", DISTRO_ARCH,
>>>> +            "--list-format", "${version}",
>>>> +            "list", DEBDISTRONAME, PN,
>>>> +        ])
>>>> +        package_version = package_version.decode("utf-8")
>>>> +        if package_version == PV:
>>>> +            # The below list contains the names of all the tasks
>>>> are in charge
>>>> +            # of building the package when the cache isn't enabled
>>>> or if the
>>>> +            # package hasn't been placed in it already.
>>>> +            # As all tasks are enabled by default, we prevent
>>>> their execution
>>>> +            # by setting the `noexec` flag, which will prevent a
>>>> rebuild of
>>>> +            # the package when it's cached.
>>>> +            for task in ["fetch", "unpack", "build", "install"]:
>>>> +                d.setVarFlag("do_{}".format(task), "noexec", "1")
>>>> +
>>>> +            # Cache the results of this command so that subsequent
>>>> executions
>>>> +            # of this anonymous functions don't run the same code
>>>> again.
>>>> +            pd[PN] = PV
>>>> +    except subprocess.CalledProcessError as e:
>>>> +        bb.fatal("Unable to check for a candidate for package {0}
>>>> (errorcode: {1})".format(PN, e.returncode)) +}
>>>> diff --git a/meta/classes/image.bbclass
>>>> b/meta/classes/image.bbclass index a7f0d74..f42aa48 100644
>>>> --- a/meta/classes/image.bbclass
>>>> +++ b/meta/classes/image.bbclass
>>>> @@ -11,18 +11,34 @@ inherit ${IMAGE_TYPE}
>>>>    
>>>>    do_populate[stamp-extra-info] = "${MACHINE}-${DISTRO}"
>>>>    
>>>> -# Install Debian packages, that were built from sources
>>>> +# Install Debian packages from the cache
>>>>    do_populate() {
>>>> +    readonly DIR_CACHE="${DEBCACHEDIR}/${DISTRO}"
>>>> +
>>>>        if [ -n "${IMAGE_INSTALL}" ]; then
>>>> -        sudo mkdir -p ${S}/deb
>>>> +        if [ "${DEBCACHE_ENABLED}" != "0" ]; then
>>>> +            sudo mkdir -p "${S}/${DEBCACHEMNT}"
>>>> +            sudo mount -o bind "${DIR_CACHE}"
>>>> "${S}/${DEBCACHEMNT}" +
>>>> +            sudo chroot "${S}" apt-get update -y
>>>
>>> This will fail if you are behind a proxy. It makes a known issue
>>> worse.
>>>> +            for package in ${IMAGE_INSTALL}; do
>>>> +                sudo chroot "${S}" apt-get install -t
>>>> "${DEBDISTRONAME}" -y \
>>>> +                    --allow-unauthenticated "${package}"
>>>> +            done
>>>> +
>>>> +            sudo umount "${S}/${DEBCACHEMNT}"
>>>
>>> A whole lot of sudo. I do not mind but am still wondering ...
>>
>> Please questions and proposals instead of negative tone.
> 
> That was maybe slightly negative. Why? Because that patch was sent at a
> time where the policy was still "do not add more sudos". And the patch
> violates more of the groundrules we have agreed on.

That is actually not true, because the policy still is "do not add more 
sudo if you have no use-case for them".

I'd like to cover this topic again to try to explain the issue. The 
thread was started about your comment:

 > Some security enhancing packages can cause our initrd to be not
 > readable by a normal user. So we need to copy with sudo.
 > Also regular cp would destroy ownership and other attributes of files,
 > possibly creating problems in the future.

and in the patch you added three sudo instead of single one for initrd. 
No problem with sudo for initrd - you provided use-case. But "possibly 
creating problems in the future" doesn't provide use-case for the 
remaining two entries.

> You posting the patch signals that our groundrules seem to apply
> to !ilbers-contributions only.
> 
>>> Can we replace the bind-mount with a symlink? If anything between
>>> the mount and umount fails and you need to run again ... will the
>>> mount fail if the old one is still there?
>>
>> If we switch to multistrap-based populating, I think this will go.
> 
> I know, but we have not decided that yet.
> 
> Henning
>   
>>>    
>>>> +        else
>>>> +            sudo mkdir -p ${S}/deb
>>>>    
>>>> -        for p in ${IMAGE_INSTALL}; do
>>>> -            sudo cp ${DEPLOY_DIR_DEB}/${p}_*.deb ${S}/deb
>>>> -        done
>>>> +            for p in ${IMAGE_INSTALL}; do
>>>> +                find "${DEPLOY_DIR_DEB}" -type f -name '*.deb'
>>>> -exec \
>>>> +                    sudo cp '{}' "${S}/deb/" \;
>>>> +            done
>>>>    
>>>> -        sudo chroot ${S} /usr/bin/dpkg -i -R /deb
>>>> +            sudo chroot ${S} /usr/bin/dpkg -i -R /deb
>>>>    
>>>> -        sudo rm -rf ${S}/deb
>>>> +            sudo rm -rf ${S}/deb
>>>> +        fi
>>>>        fi
>>>>    }
>>>>      
>>>    
>

  reply	other threads:[~2017-08-29  8:20 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2017-08-27 15:13 [PATCH 0/6] Isar apt cache implementation Alexander Smirnov
2017-08-27 15:13 ` [PATCH 1/6] meta-isar-bin: Enable caching of deb packages Alexander Smirnov
2017-08-28 15:18   ` Henning Schild
2017-08-29  6:40     ` Alexander Smirnov
2017-08-29  7:51       ` Henning Schild
2017-08-29  8:20         ` Alexander Smirnov [this message]
2017-08-31 10:55     ` Claudius Heine
2017-08-31 11:20       ` Henning Schild
2017-08-31 12:08         ` Claudius Heine
2017-09-06 14:21   ` Henning Schild
2017-09-07 11:13     ` Claudius Heine
2017-08-27 15:13 ` [PATCH 2/6] classes/image: Provide /dev/null for Stretch apt Alexander Smirnov
2017-08-28 15:20   ` Henning Schild
2017-08-28 15:26     ` Henning Schild
2017-08-27 15:13 ` [PATCH 3/6] classes/dpkg: Split install for cache Alexander Smirnov
2017-08-28  8:00   ` Claudius Heine
2017-08-29  7:18     ` Alexander Smirnov
2017-08-30  8:54       ` Claudius Heine
2017-08-28 15:30   ` Henning Schild
2017-08-27 15:13 ` [PATCH 4/6] meta-isar-bin: Enable apt repo generation for amd64 Alexander Smirnov
2017-08-27 15:13 ` [PATCH 5/6] classes/dpkg: Properly update packages in the cache Alexander Smirnov
2017-08-28 15:32   ` Henning Schild
2017-08-29  7:20     ` Alexander Smirnov
2017-08-29  7:57       ` Henning Schild
2017-08-29 11:26         ` Jan Kiszka
2017-08-27 15:13 ` [PATCH 6/6] doc/technical_overview: Describe binary cache Alexander Smirnov
2017-08-28 15:36   ` Henning Schild
2017-08-29  7:29     ` Alexander Smirnov
2017-08-29  8:06       ` Henning Schild
2017-08-29 11:29     ` Jan Kiszka

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=d2d53aaf-d3c0-267e-bd58-87b840525ab1@ilbers.de \
    --to=asmirnov@ilbers.de \
    --cc=henning.schild@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