comparison intree-build-helper/cutils_build.py @ 406:deabdfed3b96

For current Python versions use an intree build backend that wraps setuptools.build_meta. Augmenting the generated sdist archive is now done automatically.
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 17 Feb 2026 16:15:31 +0100
parents
children
comparison
equal deleted inserted replaced
405:8e4ece6fad74 406:deabdfed3b96
1 # -*- coding: utf-8 -*-
2 # :-
3 # SPDX-FileCopyrightText: © 2025-2026 Franz Glasner
4 # SPDX-License-Identifier: BSD-3-Clause
5 # :-
6 r"""An intree build backend that is mostly just a wrapper for
7 :mod:`setuptools.build_meta`.
8
9 Augments :fn:`build_sdist` to automatically postprocess it
10 (i.e. add symlinks as symlinks).
11
12 """
13
14 # Firstly, we are just a wrapper for setuptools.build_meta
15 from setuptools.build_meta import * # noqa:F403,F401
16 from setuptools.build_meta import build_sdist as _orig_build_sdist
17
18 import gzip as _gzip
19 import io as _io
20 import logging as _logging
21 import os as _os
22 import tarfile as _tarfile
23
24
25 _log = _logging.getLogger(__name__)
26
27
28 def _postprocess_sdist(sdist_directory, sdist_archive, config_settings):
29 _log.info("post-processing the sdist %r ...", sdist_archive)
30 #
31 # PEP 625 requires that sdist archive filenames are of the form
32 # <normalized_project_name>-<project_version>.tar.gz
33 #
34 if sdist_archive.endswith(".tar.gz"):
35 uncompressed_sdist_archive = sdist_archive[:-3]
36 # the directory prefix within the archive
37 archive_path_prefix = uncompressed_sdist_archive[:-4]
38 normalized_project_name, sep, project_version = \
39 archive_path_prefix.rpartition('-')
40 if not sep:
41 raise ValueError(
42 "unexpected archive path prefix: %s" % (archive_path_prefix,))
43 else:
44 raise ValueError("unexpected archive name: %s" % (sdist_archive,))
45
46 uncompressed_sdist_path = f"{sdist_directory}/{uncompressed_sdist_archive}"
47
48 # Metadata directories in the FS and the archive
49 egg_directory = f"{normalized_project_name}.egg-info"
50 if not _os.path.isdir(egg_directory):
51 raise RuntimeError("directory does not exist: %s" % (egg_directory,))
52 sources_txt_path = f"{egg_directory}/SOURCES.txt"
53 sources_txt_arcname = f"{archive_path_prefix}/{egg_directory}/SOURCES.txt"
54
55 if _os.path.isfile(uncompressed_sdist_path):
56 _log.warning("warning: overwriting existing %r",
57 uncompressed_sdist_path)
58
59 # Uncompress
60 _log.info("uncompressing the created archive %r into %r",
61 f"{sdist_directory}/{sdist_archive}",
62 uncompressed_sdist_path)
63 with _gzip.GzipFile(f"{sdist_directory}/{sdist_archive}",
64 mode="rb") as ca:
65 with open(uncompressed_sdist_path, "wb") as uca:
66 while True:
67 data = ca.read(64*1024)
68 if not data:
69 break
70 uca.write(data)
71
72 # Get SOURCES.txt from the metadata within the sdist
73 with _tarfile.TarFile(uncompressed_sdist_path, "r") as tf:
74 sf = tf.extractfile(sources_txt_arcname)
75 try:
76 sources_txt = sf.read()
77 finally:
78 sf.close()
79
80 with _tarfile.TarFile(uncompressed_sdist_path, "a") as tf:
81 arcname = "{}/tests/data".format(archive_path_prefix)
82 try:
83 info = tf.getmember(arcname)
84 except KeyError:
85 pass
86 else:
87 raise RuntimeError("already postprocessed")
88 pre_names = set(tf.getnames())
89 tf.add("tests/data", arcname=arcname, recursive=True)
90
91 #
92 # Determine the new files and symlinks that are to be added
93 # to SOURCES.txt. Skip directories.
94 #
95 post_names = set(tf.getnames())
96 new_names = list(post_names - pre_names)
97 new_names.sort()
98 new_sources = []
99
100 for np in new_names:
101 nn = np[len(archive_path_prefix)+1:]
102 info = tf.getmember(np)
103 if not info.isdir():
104 _log.info("adding %s -> %s", nn, np)
105 new_sources.append(nn)
106
107 # Augment SOURCES.txt and add it to the archive
108 sources_info = tf.gettarinfo(
109 sources_txt_path, arcname=sources_txt_arcname)
110 sf = _io.BytesIO()
111 sf.write(sources_txt)
112 if not sources_txt.endswith(b'\n'):
113 sf.write(b'\n')
114 sf.write(_b('\n'.join(new_sources)))
115 sources_info.size = len(sf.getvalue())
116 sf.seek(0)
117 #
118 # This adds SOURCES.txt a 2nd time -- effectively overwriting
119 # the "earlier" one.
120 #
121 tf.addfile(sources_info, sf)
122
123 # Compress
124 _log.info("recompressing the augmented archive %r into %r",
125 uncompressed_sdist_path,
126 f"{sdist_directory}/{sdist_archive}")
127 with open(uncompressed_sdist_path, "rb") as uca:
128 with open(f"{sdist_directory}/{sdist_archive}", "wb") as ca:
129 with _gzip.GzipFile(filename=uncompressed_sdist_archive,
130 fileobj=ca,
131 mode="wb") as gzfile:
132 while True:
133 data = uca.read(64*1024)
134 if not data:
135 break
136 gzfile.write(data)
137
138 _log.info("post-processing the sdist done.")
139 return sdist_archive
140
141
142 def _b(buf, encoding="ascii"):
143 if isinstance(buf, bytes):
144 return buf
145 else:
146 return buf.encode(encoding)
147
148
149 def build_sdist(sdist_directory, config_settings=None):
150 # NOTE: logging is obviously set to level WARN (default?)
151 _log.debug(
152 "debug: build_sdist in cutils_build called with params"
153 " sdist_directory=%r, config_settings=%r",
154 sdist_directory, config_settings)
155 # NOTE: orig_build_sdist re-configures logging to level INFO
156 sdist_archive = _orig_build_sdist(
157 sdist_directory, config_settings=config_settings)
158 return _postprocess_sdist(sdist_directory, sdist_archive, config_settings)