diff mbox series

[libgpiod,v4,1/1] bindings: python: optionally include module in sdist

Message ID 20231016151848.168209-2-phil@gadgetoid.com
State New
Headers show
Series bindings: python: optionally include module in sdist | expand

Commit Message

Phil Howard Oct. 16, 2023, 3:18 p.m. UTC
Optionally vendor libgpiod source into sdist so that the
Python module can be built from source, even with a missing
or mismatched system libgpiod.

Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
so that the sdist package can optionally be built and
linked against a compatible system libgpiod.

eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod

Update build to add an additional sdist target for upload
to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
triggers a vendored package build. "GPIOD_VERSION_STR" is
saved to "gpiod-version-str.txt" and included in the sdist
for standalone builds.

"GPIOD_VERSION_STR" must be specified in order to produce
a standalone buildable sdist package, this requirement
implicitly preserves the old build behaviour.

Signed-off-by: Phil Howard <phil@gadgetoid.com>
---
 bindings/python/MANIFEST.in |   5 ++
 bindings/python/Makefile.am |   3 +
 bindings/python/setup.py    | 122 +++++++++++++++++++++++++++++++-----
 3 files changed, 114 insertions(+), 16 deletions(-)

Comments

Bartosz Golaszewski Oct. 17, 2023, 10:08 a.m. UTC | #1
On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
>
> Optionally vendor libgpiod source into sdist so that the
> Python module can be built from source, even with a missing
> or mismatched system libgpiod.
>
> Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> so that the sdist package can optionally be built and
> linked against a compatible system libgpiod.
>
> eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
>
> Update build to add an additional sdist target for upload
> to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> triggers a vendored package build. "GPIOD_VERSION_STR" is
> saved to "gpiod-version-str.txt" and included in the sdist
> for standalone builds.
>
> "GPIOD_VERSION_STR" must be specified in order to produce
> a standalone buildable sdist package, this requirement
> implicitly preserves the old build behaviour.
>
> Signed-off-by: Phil Howard <phil@gadgetoid.com>
> ---

Alright, this looks and works well. Before I merge it: the package
still shows up in 'pip freeze' as libgpiod. Now that we control the
name 'gpiod' on pypi, should we change it to 'gpiod' instead?

Once I apply the patch - how would we go about making a new proper
release? Do you also want to add a better homepage to setup.py so that
users of pypi don't bounce off the nonexistent documentation?

Bart
Phil Howard Oct. 17, 2023, 11:15 a.m. UTC | #2
On Tue, 17 Oct 2023 at 11:09, Bartosz Golaszewski <brgl@bgdev.pl> wrote:
>
> On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
> >
> > Optionally vendor libgpiod source into sdist so that the
> > Python module can be built from source, even with a missing
> > or mismatched system libgpiod.
> >
> > Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> > so that the sdist package can optionally be built and
> > linked against a compatible system libgpiod.
> >
> > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
> >
> > Update build to add an additional sdist target for upload
> > to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> > triggers a vendored package build. "GPIOD_VERSION_STR" is
> > saved to "gpiod-version-str.txt" and included in the sdist
> > for standalone builds.
> >
> > "GPIOD_VERSION_STR" must be specified in order to produce
> > a standalone buildable sdist package, this requirement
> > implicitly preserves the old build behaviour.
> >
> > Signed-off-by: Phil Howard <phil@gadgetoid.com>
> > ---
>
> Alright, this looks and works well. Before I merge it: the package
> still shows up in 'pip freeze' as libgpiod. Now that we control the
> name 'gpiod' on pypi, should we change it to 'gpiod' instead?

Yes, it's conventional to have the module and package name match
for single module packages.

> Once I apply the patch - how would we go about making a new proper
> release?

The "libgpiod-2.0.1.tar.gz" (or "gpiod-2.0.1.tar.gz" if we change it) should
be uploaded to pypi via the utility "twine" (pip install twine). You can upload
versions to testpypi by specifying "--repository testpypi" which, combined
with a "-postN" version suffix, can help work through bugs in packaging or
README formatting.

You can also run "twine check dist/*" to precheck for packaging issues.

I'm not sure if we should yank prior releases of the Python gpiod but
some note in the README paying lip service to the change from unofficial
Python to official C-bindings would be wise.

> Do you also want to add a better homepage to setup.py so that
> users of pypi don't bounce off the nonexistent documentation?

Yes, we need a basic introduction and a couple of succinct examples.

I have some half done edits to the README somewhere locally which I
was holding back until after we got the packaging working and until I
could ask: What format would you prefer for the README?

>
> Bart
Bartosz Golaszewski Oct. 17, 2023, 11:29 a.m. UTC | #3
On Tue, Oct 17, 2023 at 1:15 PM Phil Howard <phil@gadgetoid.com> wrote:
>
> On Tue, 17 Oct 2023 at 11:09, Bartosz Golaszewski <brgl@bgdev.pl> wrote:
> >
> > On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
> > >
> > > Optionally vendor libgpiod source into sdist so that the
> > > Python module can be built from source, even with a missing
> > > or mismatched system libgpiod.
> > >
> > > Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> > > so that the sdist package can optionally be built and
> > > linked against a compatible system libgpiod.
> > >
> > > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
> > >
> > > Update build to add an additional sdist target for upload
> > > to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> > > triggers a vendored package build. "GPIOD_VERSION_STR" is
> > > saved to "gpiod-version-str.txt" and included in the sdist
> > > for standalone builds.
> > >
> > > "GPIOD_VERSION_STR" must be specified in order to produce
> > > a standalone buildable sdist package, this requirement
> > > implicitly preserves the old build behaviour.
> > >
> > > Signed-off-by: Phil Howard <phil@gadgetoid.com>
> > > ---
> >
> > Alright, this looks and works well. Before I merge it: the package
> > still shows up in 'pip freeze' as libgpiod. Now that we control the
> > name 'gpiod' on pypi, should we change it to 'gpiod' instead?
>
> Yes, it's conventional to have the module and package name match
> for single module packages.
>
> > Once I apply the patch - how would we go about making a new proper
> > release?
>
> The "libgpiod-2.0.1.tar.gz" (or "gpiod-2.0.1.tar.gz" if we change it) should
> be uploaded to pypi via the utility "twine" (pip install twine). You can upload
> versions to testpypi by specifying "--repository testpypi" which, combined
> with a "-postN" version suffix, can help work through bugs in packaging or
> README formatting.
>
> You can also run "twine check dist/*" to precheck for packaging issues.

Sure, this is how I uploaded the existing releases. I was thinking
more about versioning. We haven't added new interfaces to the code so
I think the version should be set to v2.0.2 as technically this is a
bug-fix release + build improvements.

>
> I'm not sure if we should yank prior releases of the Python gpiod but
> some note in the README paying lip service to the change from unofficial
> Python to official C-bindings would be wise.
>

You mean out of the "gpiod" repo, not "libgpiod"? Because I assume
this is where we'll upload the new release.

> > Do you also want to add a better homepage to setup.py so that
> > users of pypi don't bounce off the nonexistent documentation?
>
> Yes, we need a basic introduction and a couple of succinct examples.
>
> I have some half done edits to the README somewhere locally which I
> was holding back until after we got the packaging working and until I
> could ask: What format would you prefer for the README?
>

Whatever works best or is the standard for pypi. You are the expert. :)

Bart

> >
> > Bart
Bartosz Golaszewski Oct. 17, 2023, 11:30 a.m. UTC | #4
On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
>
> Optionally vendor libgpiod source into sdist so that the
> Python module can be built from source, even with a missing
> or mismatched system libgpiod.
>
> Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> so that the sdist package can optionally be built and
> linked against a compatible system libgpiod.
>
> eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
>
> Update build to add an additional sdist target for upload
> to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> triggers a vendored package build. "GPIOD_VERSION_STR" is
> saved to "gpiod-version-str.txt" and included in the sdist
> for standalone builds.
>
> "GPIOD_VERSION_STR" must be specified in order to produce
> a standalone buildable sdist package, this requirement
> implicitly preserves the old build behaviour.
>
> Signed-off-by: Phil Howard <phil@gadgetoid.com>
> ---
>  bindings/python/MANIFEST.in |   5 ++
>  bindings/python/Makefile.am |   3 +
>  bindings/python/setup.py    | 122 +++++++++++++++++++++++++++++++-----
>  3 files changed, 114 insertions(+), 16 deletions(-)
>
> diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
> index c7124d4..acf9391 100644
> --- a/bindings/python/MANIFEST.in
> +++ b/bindings/python/MANIFEST.in
> @@ -2,6 +2,7 @@
>  # SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
>
>  include setup.py
> +include gpiod-version-str.txt
>
>  recursive-include gpiod *.py
>  recursive-include tests *.py
> @@ -11,3 +12,7 @@ recursive-include gpiod/ext *.h
>
>  recursive-include tests/gpiosim *.c
>  recursive-include tests/procname *.c
> +
> +recursive-include lib *.c
> +recursive-include lib *.h
> +recursive-include include *.h
> diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
> index 079ceb1..7fadf52 100644
> --- a/bindings/python/Makefile.am
> +++ b/bindings/python/Makefile.am
> @@ -16,6 +16,9 @@ all-local:
>         $(PYTHON) setup.py build_ext --inplace \
>                 --include-dirs=$(top_srcdir)/include/:$(top_srcdir)/tests/gpiosim/ \
>                 --library-dirs=$(top_builddir)/lib/.libs/:$(top_srcdir)/tests/gpiosim/.libs/
> +       GPIOD_VERSION_STR=$(VERSION_STR) \

I suppose I can remove this line now?

Bart

[snip]
Phil Howard Oct. 17, 2023, 12:59 p.m. UTC | #5
On Tue, 17 Oct 2023 at 12:30, Bartosz Golaszewski <brgl@bgdev.pl> wrote:
>
> On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
> >
> > Optionally vendor libgpiod source into sdist so that the
> > Python module can be built from source, even with a missing
> > or mismatched system libgpiod.
> >
> > Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> > so that the sdist package can optionally be built and
> > linked against a compatible system libgpiod.
> >
> > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
> >
> > Update build to add an additional sdist target for upload
> > to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> > triggers a vendored package build. "GPIOD_VERSION_STR" is
> > saved to "gpiod-version-str.txt" and included in the sdist
> > for standalone builds.
> >
> > "GPIOD_VERSION_STR" must be specified in order to produce
> > a standalone buildable sdist package, this requirement
> > implicitly preserves the old build behaviour.
> >
> > Signed-off-by: Phil Howard <phil@gadgetoid.com>
> > ---
> >  bindings/python/MANIFEST.in |   5 ++
> >  bindings/python/Makefile.am |   3 +
> >  bindings/python/setup.py    | 122 +++++++++++++++++++++++++++++++-----
> >  3 files changed, 114 insertions(+), 16 deletions(-)
> >
> > diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
> > index c7124d4..acf9391 100644
> > --- a/bindings/python/MANIFEST.in
> > +++ b/bindings/python/MANIFEST.in
> > @@ -2,6 +2,7 @@
> >  # SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
> >
> >  include setup.py
> > +include gpiod-version-str.txt
> >
> >  recursive-include gpiod *.py
> >  recursive-include tests *.py
> > @@ -11,3 +12,7 @@ recursive-include gpiod/ext *.h
> >
> >  recursive-include tests/gpiosim *.c
> >  recursive-include tests/procname *.c
> > +
> > +recursive-include lib *.c
> > +recursive-include lib *.h
> > +recursive-include include *.h
> > diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
> > index 079ceb1..7fadf52 100644
> > --- a/bindings/python/Makefile.am
> > +++ b/bindings/python/Makefile.am
> > @@ -16,6 +16,9 @@ all-local:
> >         $(PYTHON) setup.py build_ext --inplace \
> >                 --include-dirs=$(top_srcdir)/include/:$(top_srcdir)/tests/gpiosim/ \
> >                 --library-dirs=$(top_builddir)/lib/.libs/:$(top_srcdir)/tests/gpiosim/.libs/
> > +       GPIOD_VERSION_STR=$(VERSION_STR) \
>
> I suppose I can remove this line now?

This is right before a separate invocation of the build, which might want
to be split into another Makefile target for clarity.

This is required to build an sdist package which includes a valid
"gpiod-version-str.txt" and can be built standalone, but doesn't change
the original "build_ext" call.

>
> Bart
>
> [snip]
Phil Howard Oct. 17, 2023, 1:11 p.m. UTC | #6
On Tue, 17 Oct 2023 at 12:29, Bartosz Golaszewski <brgl@bgdev.pl> wrote:
>
> On Tue, Oct 17, 2023 at 1:15 PM Phil Howard <phil@gadgetoid.com> wrote:
> >
> > On Tue, 17 Oct 2023 at 11:09, Bartosz Golaszewski <brgl@bgdev.pl> wrote:
> > >
> > > On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
> > > >
> > > > Optionally vendor libgpiod source into sdist so that the
> > > > Python module can be built from source, even with a missing
> > > > or mismatched system libgpiod.
> > > >
> > > > Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> > > > so that the sdist package can optionally be built and
> > > > linked against a compatible system libgpiod.
> > > >
> > > > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
> > > >
> > > > Update build to add an additional sdist target for upload
> > > > to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> > > > triggers a vendored package build. "GPIOD_VERSION_STR" is
> > > > saved to "gpiod-version-str.txt" and included in the sdist
> > > > for standalone builds.
> > > >
> > > > "GPIOD_VERSION_STR" must be specified in order to produce
> > > > a standalone buildable sdist package, this requirement
> > > > implicitly preserves the old build behaviour.
> > > >
> > > > Signed-off-by: Phil Howard <phil@gadgetoid.com>
> > > > ---
> > >
> > > Alright, this looks and works well. Before I merge it: the package
> > > still shows up in 'pip freeze' as libgpiod. Now that we control the
> > > name 'gpiod' on pypi, should we change it to 'gpiod' instead?
> >
> > Yes, it's conventional to have the module and package name match
> > for single module packages.
> >
> > > Once I apply the patch - how would we go about making a new proper
> > > release?
> >
> > The "libgpiod-2.0.1.tar.gz" (or "gpiod-2.0.1.tar.gz" if we change it) should
> > be uploaded to pypi via the utility "twine" (pip install twine). You can upload
> > versions to testpypi by specifying "--repository testpypi" which, combined
> > with a "-postN" version suffix, can help work through bugs in packaging or
> > README formatting.
> >
> > You can also run "twine check dist/*" to precheck for packaging issues.
>
> Sure, this is how I uploaded the existing releases. I was thinking
> more about versioning. We haven't added new interfaces to the code so
> I think the version should be set to v2.0.2 as technically this is a
> bug-fix release + build improvements.

Agreed.

> >
> > I'm not sure if we should yank prior releases of the Python gpiod but
> > some note in the README paying lip service to the change from unofficial
> > Python to official C-bindings would be wise.
> >
>
> You mean out of the "gpiod" repo, not "libgpiod"? Because I assume
> this is where we'll upload the new release.

Yes since new packages will be uploaded to the "gpiod" pypi repo they
are going to be seen as an "update" to the old "gpiod".

Something like "For the old unofficial Python gpiod install with:
pip install gpiod==1.5.4" should suffice.

There's good evidence [1] that gpiod is at least moderately popular so we
should expect some minor fallout from people who refuse to pin their deps.

[1] - https://pypistats.org/packages/gpiod

>
> > > Do you also want to add a better homepage to setup.py so that
> > > users of pypi don't bounce off the nonexistent documentation?
> >
> > Yes, we need a basic introduction and a couple of succinct examples.
> >
> > I have some half done edits to the README somewhere locally which I
> > was holding back until after we got the packaging working and until I
> > could ask: What format would you prefer for the README?
> >
>
> Whatever works best or is the standard for pypi. You are the expert. :)

I prefer Markdown because it's what I know and use. I don't believe
any specific format is preferred these days- though the fallback default is
'text/x-rst'

>
> Bart
>
> > >
> > > Bart
Bartosz Golaszewski Oct. 17, 2023, 3:40 p.m. UTC | #7
On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
>
> Optionally vendor libgpiod source into sdist so that the
> Python module can be built from source, even with a missing
> or mismatched system libgpiod.
>
> Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> so that the sdist package can optionally be built and
> linked against a compatible system libgpiod.
>
> eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
>
> Update build to add an additional sdist target for upload
> to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> triggers a vendored package build. "GPIOD_VERSION_STR" is
> saved to "gpiod-version-str.txt" and included in the sdist
> for standalone builds.
>
> "GPIOD_VERSION_STR" must be specified in order to produce
> a standalone buildable sdist package, this requirement
> implicitly preserves the old build behaviour.
>
> Signed-off-by: Phil Howard <phil@gadgetoid.com>
> ---

I just realized one issue with this. Even if we indicate the version
of libgpiod as v2.0.2 using the environment variable but actually
create it from the master branch then the build-in code will really be
the development branch not v2.0.2 stable release.

This is precisely the kind of issue that linking to system lib allows
to avoid. :)

Anyway, maybe we would need an additional environment variable that
could be used to point setup.py to the actual source tree we want to
use, which may be different from the tree we're using to generate the
sdist package? Typically we'd want to release the pypi package with
the most recent stable release of libgpiod.

Bart
Bartosz Golaszewski Oct. 17, 2023, 3:52 p.m. UTC | #8
On Tue, Oct 17, 2023 at 5:40 PM Bartosz Golaszewski <brgl@bgdev.pl> wrote:
>
> On Mon, Oct 16, 2023 at 5:18 PM Phil Howard <phil@gadgetoid.com> wrote:
> >
> > Optionally vendor libgpiod source into sdist so that the
> > Python module can be built from source, even with a missing
> > or mismatched system libgpiod.
> >
> > Add optional environment variable "LINK_SYSTEM_LIBGPIOD=1"
> > so that the sdist package can optionally be built and
> > linked against a compatible system libgpiod.
> >
> > eg: LINK_SYSTEM_LIBGPIOD=1 pip install libgpiod
> >
> > Update build to add an additional sdist target for upload
> > to pypi. Call setup.py with "GPIOD_VERSION_STR" set, which
> > triggers a vendored package build. "GPIOD_VERSION_STR" is
> > saved to "gpiod-version-str.txt" and included in the sdist
> > for standalone builds.
> >
> > "GPIOD_VERSION_STR" must be specified in order to produce
> > a standalone buildable sdist package, this requirement
> > implicitly preserves the old build behaviour.
> >
> > Signed-off-by: Phil Howard <phil@gadgetoid.com>
> > ---
>
> I just realized one issue with this. Even if we indicate the version
> of libgpiod as v2.0.2 using the environment variable but actually
> create it from the master branch then the build-in code will really be
> the development branch not v2.0.2 stable release.
>
> This is precisely the kind of issue that linking to system lib allows
> to avoid. :)
>
> Anyway, maybe we would need an additional environment variable that
> could be used to point setup.py to the actual source tree we want to
> use, which may be different from the tree we're using to generate the
> sdist package? Typically we'd want to release the pypi package with
> the most recent stable release of libgpiod.
>
> Bart

Or we could even do it online and fetch the libgpiod tarball from the
official repo[1] at kernel.org?

Bart

[1] https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/
diff mbox series

Patch

diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
index c7124d4..acf9391 100644
--- a/bindings/python/MANIFEST.in
+++ b/bindings/python/MANIFEST.in
@@ -2,6 +2,7 @@ 
 # SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
 
 include setup.py
+include gpiod-version-str.txt
 
 recursive-include gpiod *.py
 recursive-include tests *.py
@@ -11,3 +12,7 @@  recursive-include gpiod/ext *.h
 
 recursive-include tests/gpiosim *.c
 recursive-include tests/procname *.c
+
+recursive-include lib *.c
+recursive-include lib *.h
+recursive-include include *.h
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
index 079ceb1..7fadf52 100644
--- a/bindings/python/Makefile.am
+++ b/bindings/python/Makefile.am
@@ -16,6 +16,9 @@  all-local:
 	$(PYTHON) setup.py build_ext --inplace \
 		--include-dirs=$(top_srcdir)/include/:$(top_srcdir)/tests/gpiosim/ \
 		--library-dirs=$(top_builddir)/lib/.libs/:$(top_srcdir)/tests/gpiosim/.libs/
+	GPIOD_VERSION_STR=$(VERSION_STR) \
+	$(PYTHON) setup.py sdist
+
 
 install-exec-local:
 	GPIOD_WITH_TESTS= \
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
index df10e18..168c1af 100644
--- a/bindings/python/setup.py
+++ b/bindings/python/setup.py
@@ -1,10 +1,49 @@ 
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
-from os import environ, path
+from os import environ, path, unlink
 from setuptools import setup, Extension, find_packages
 from setuptools.command.build_ext import build_ext as orig_build_ext
-from shutil import rmtree
+from setuptools.command.sdist import sdist as orig_sdist
+from shutil import rmtree, copytree
+
+
+def get_gpiod_version_str():
+    try:
+        return environ["GPIOD_VERSION_STR"]
+    except KeyError:
+        pass
+    try:
+        return open("gpiod-version-str.txt", "r").read()
+    except OSError:
+        return None
+
+
+def copy_libgpiod_files(func):
+    """
+    In order to include the lib and include directories in the sdist
+    we must temporarily copy them up into the python bindings directory.
+
+    If "./lib" exists we are building from an sdist package and will not
+    try to copy the files again.
+    """
+
+    def wrapper(self):
+        copy_src = not path.exists("./lib")
+        if copy_src:
+            gpiod_version_str = get_gpiod_version_str()
+            if gpiod_version_str is not None:
+                open("gpiod-version-str.txt", "w").write(gpiod_version_str)
+            copytree("../../lib", "./lib")
+            copytree("../../include", "./include")
+        func(self)
+        if copy_src:
+            if gpiod_version_str is not None:
+                unlink("gpiod-version-str.txt")
+            rmtree("./lib")
+            rmtree("./include")
+
+    return wrapper
 
 
 class build_ext(orig_build_ext):
@@ -14,24 +53,78 @@  class build_ext(orig_build_ext):
     were built (and possibly copied to the source directory if inplace is set).
     """
 
+    @copy_libgpiod_files
     def run(self):
         super().run()
         rmtree(path.join(self.build_lib, "tests"), ignore_errors=True)
 
 
+class sdist(orig_sdist):
+    """
+    Wrap sdist so that we can copy the lib and include files into . where
+    MANIFEST.in will include them in the source package.
+    """
+
+    @copy_libgpiod_files
+    def run(self):
+        super().run()
+
+
+with open("gpiod/version.py", "r") as fd:
+    exec(fd.read())
+
+sources = [
+    # gpiod Python bindings
+    "gpiod/ext/chip.c",
+    "gpiod/ext/common.c",
+    "gpiod/ext/line-config.c",
+    "gpiod/ext/line-settings.c",
+    "gpiod/ext/module.c",
+    "gpiod/ext/request.c",
+]
+
+extra_compile_args = [
+    "-Wall",
+    "-Wextra",
+]
+
+libraries = ["gpiod"]
+include_dirs = ["gpiod"]
+
+if environ.get("LINK_SYSTEM_LIBGPIOD") == "1":
+    print("linking system libgpiod (requested by LINK_SYSTEM_LIBGPIOD)")
+elif get_gpiod_version_str() is None:
+    print("warning: linking system libgpiod (GPIOD_VERSION_STR not specified)")
+else:
+    print("vendoring libgpiod into standalone library")
+    sources += [
+        # gpiod library
+        "lib/chip.c",
+        "lib/chip-info.c",
+        "lib/edge-event.c",
+        "lib/info-event.c",
+        "lib/internal.c",
+        "lib/line-config.c",
+        "lib/line-info.c",
+        "lib/line-request.c",
+        "lib/line-settings.c",
+        "lib/misc.c",
+        "lib/request-config.c",
+    ]
+    libraries = []
+    include_dirs = ["include", "lib", "gpiod/ext"]
+    extra_compile_args += [
+        '-DGPIOD_VERSION_STR="{}"'.format(get_gpiod_version_str()),
+    ]
+
+
 gpiod_ext = Extension(
     "gpiod._ext",
-    sources=[
-        "gpiod/ext/chip.c",
-        "gpiod/ext/common.c",
-        "gpiod/ext/line-config.c",
-        "gpiod/ext/line-settings.c",
-        "gpiod/ext/module.c",
-        "gpiod/ext/request.c",
-    ],
+    libraries=libraries,
+    sources=sources,
     define_macros=[("_GNU_SOURCE", "1")],
-    libraries=["gpiod"],
-    extra_compile_args=["-Wall", "-Wextra"],
+    include_dirs=include_dirs,
+    extra_compile_args=extra_compile_args,
 )
 
 gpiosim_ext = Extension(
@@ -54,15 +147,12 @@  if environ.get("GPIOD_WITH_TESTS") == "1":
     extensions.append(gpiosim_ext)
     extensions.append(procname_ext)
 
-with open("gpiod/version.py", "r") as fd:
-    exec(fd.read())
-
 setup(
     name="libgpiod",
     packages=find_packages(exclude=["tests", "tests.*"]),
     python_requires=">=3.9.0",
     ext_modules=extensions,
-    cmdclass={"build_ext": build_ext},
+    cmdclass={"build_ext": build_ext, "sdist": sdist},
     version=__version__,
     author="Bartosz Golaszewski",
     author_email="brgl@bgdev.pl",