Gentoo Archives: gentoo-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-dev@l.g.o
Cc: "Michał Górny" <mgorny@g.o>
Subject: [gentoo-dev] [PATCH v2] distutils-r1.eclass: Implement PEP517 mode
Date: Fri, 14 Jan 2022 15:18:58
Message-Id: 20220114151846.675647-1-mgorny@gentoo.org
1 Add a PEP517 mode to the distutils-r1.eclass in order to facilitate
2 building packages via PEP517 backends. In order to use it, set
3 DISTUTILS_USE_PEP517 to the appropriate build system name. The eclass
4 will take care of setting BDEPEND, then invoke the backend to build
5 a wheel and then install its contents in python_compile(). The install
6 phase is limited to merging the staging area into the image directory.
7
8 In PEP517 mode, the test phase is automatically provided with venv-style
9 install tree that should suffice the vast majority of test suites.
10 As a result, distutils_install_for_testing should no longer be necessary
11 and is not available in this mode.
12
13 The new mode can also be used to install pre-PEP517 distutils
14 and setuptools packages. To do so, just specify setuptools backend.
15 If pyproject.toml is missing, the eclass assumes legacy setuptools
16 backend that invokes setup.py. It also enables setuptools-vendored
17 distutils, effectively carrying the migration from deprecated stdlib
18 version.
19
20 The PEP517 support effectively deprecates the legacy eclass mode.
21 This follows upstream deprecation of distutils and install commands
22 in setuptools.
23
24 Signed-off-by: Michał Górny <mgorny@g.o>
25 ---
26 eclass/distutils-r1.eclass | 381 ++++++++++++++++++++++++++++---------
27 1 file changed, 291 insertions(+), 90 deletions(-)
28
29 Changes:
30
31 * do not override build paths as pep517 doesn't respect pydistutils.cfg
32 and this effectively means it can't find built extensions and builds
33 them again
34
35 * fix handling legacy packages with pyproject.toml that don't declare
36 build-backend
37
38 * add more backends (fdm, poetry)
39
40 diff --git a/eclass/distutils-r1.eclass b/eclass/distutils-r1.eclass
41 index ba0226f8fed3..eb8471db8a53 100644
42 --- a/eclass/distutils-r1.eclass
43 +++ b/eclass/distutils-r1.eclass
44 @@ -78,7 +78,35 @@ esac
45 # to be exported. It must be run in order for the eclass functions
46 # to function properly.
47
48 +# @ECLASS-VARIABLE: DISTUTILS_USE_PEP517
49 +# @PRE_INHERIT
50 +# @DEFAULT_UNSET
51 +# @DESCRIPTION:
52 +# Enable experimental PEP 517 mode for the specified build system.
53 +# In this mode, the complete build and install is done
54 +# in python_compile(), venv-style install tree is provided
55 +# to python_test() and python_install() just merges the temporary
56 +# install tree into real fs.
57 +#
58 +# The variable specifies the build system used. Currently,
59 +# the following values are supported:
60 +#
61 +# - flit - flit_core backend
62 +#
63 +# - pdm - pdm.pep517 backend
64 +#
65 +# - poetry - poetry-core backend
66 +#
67 +# - setuptools - distutils or setuptools (incl. legacy mode)
68 +#
69 +# - standalone - standalone build systems without external deps
70 +# (used for bootstrapping).
71 +#
72 +# The variable needs to be set before the inherit line. The eclass
73 +# adds appropriate build-time dependencies and verifies the value.
74 +
75 # @ECLASS-VARIABLE: DISTUTILS_USE_SETUPTOOLS
76 +# @DEFAULT_UNSET
77 # @PRE_INHERIT
78 # @DESCRIPTION:
79 # Controls adding dev-python/setuptools dependency. The allowed values
80 @@ -97,13 +125,13 @@ esac
81 # (assumes you will take care of doing it correctly)
82 #
83 # This variable is effective only if DISTUTILS_OPTIONAL is disabled.
84 -# It needs to be set before the inherit line.
85 -: ${DISTUTILS_USE_SETUPTOOLS:=bdepend}
86 +# It is available only in non-PEP517 mode. It needs to be set before
87 +# the inherit line.
88
89 if [[ ! ${_DISTUTILS_R1} ]]; then
90
91 [[ ${EAPI} == 6 ]] && inherit eutils xdg-utils
92 -inherit multiprocessing toolchain-funcs
93 +inherit multibuild multiprocessing toolchain-funcs
94
95 if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
96 inherit python-r1
97 @@ -121,25 +149,62 @@ if [[ ! ${_DISTUTILS_R1} ]]; then
98
99 _distutils_set_globals() {
100 local rdep bdep
101 - local setuptools_dep='>=dev-python/setuptools-42.0.2[${PYTHON_USEDEP}]'
102 + if [[ ${DISTUTILS_USE_PEP517} ]]; then
103 + if [[ ${DISTUTILS_USE_SETUPTOOLS} ]]; then
104 + die "DISTUTILS_USE_SETUPTOOLS is not used in PEP517 mode"
105 + fi
106
107 - case ${DISTUTILS_USE_SETUPTOOLS} in
108 - no|manual)
109 - ;;
110 - bdepend)
111 - bdep+=" ${setuptools_dep}"
112 - ;;
113 - rdepend)
114 - bdep+=" ${setuptools_dep}"
115 - rdep+=" ${setuptools_dep}"
116 - ;;
117 - pyproject.toml)
118 - bdep+=' >=dev-python/pyproject2setuppy-22[${PYTHON_USEDEP}]'
119 - ;;
120 - *)
121 - die "Invalid DISTUTILS_USE_SETUPTOOLS=${DISTUTILS_USE_SETUPTOOLS}"
122 - ;;
123 - esac
124 + # pip is used to install the wheel (TODO: replace with installer
125 + # once it finally gets a CLI)
126 + # tomli is used to read build-backend from pyproject.toml
127 + bdep+='
128 + dev-python/pip[${PYTHON_USEDEP}]
129 + dev-python/tomli[${PYTHON_USEDEP}]'
130 + case ${DISTUTILS_USE_PEP517} in
131 + flit)
132 + bdep+='
133 + dev-python/flit_core[${PYTHON_USEDEP}]'
134 + ;;
135 + pdm)
136 + bdep+='
137 + dev-python/pdm-pep517[${PYTHON_USEDEP}]'
138 + ;;
139 + poetry)
140 + bdep+='
141 + dev-python/poetry-core[${PYTHON_USEDEP}]'
142 + ;;
143 + setuptools)
144 + bdep+='
145 + >=dev-python/setuptools-60.5.0[${PYTHON_USEDEP}]
146 + dev-python/wheel[${PYTHON_USEDEP}]'
147 + ;;
148 + standalone)
149 + ;;
150 + *)
151 + die "Unknown DISTUTILS_USE_PEP517=${DISTUTILS_USE_PEP517}"
152 + ;;
153 + esac
154 + else
155 + local setuptools_dep='>=dev-python/setuptools-42.0.2[${PYTHON_USEDEP}]'
156 +
157 + case ${DISTUTILS_USE_SETUPTOOLS:-bdepend} in
158 + no|manual)
159 + ;;
160 + bdepend)
161 + bdep+=" ${setuptools_dep}"
162 + ;;
163 + rdepend)
164 + bdep+=" ${setuptools_dep}"
165 + rdep+=" ${setuptools_dep}"
166 + ;;
167 + pyproject.toml)
168 + bdep+=' >=dev-python/pyproject2setuppy-22[${PYTHON_USEDEP}]'
169 + ;;
170 + *)
171 + die "Invalid DISTUTILS_USE_SETUPTOOLS=${DISTUTILS_USE_SETUPTOOLS}"
172 + ;;
173 + esac
174 + fi
175
176 if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
177 bdep=${bdep//\$\{PYTHON_USEDEP\}/${PYTHON_USEDEP}}
178 @@ -469,7 +534,9 @@ esetup.py() {
179
180 _python_check_EPYTHON
181
182 - [[ ${BUILD_DIR} ]] && _distutils-r1_create_setup_cfg
183 + if [[ ${BUILD_DIR} && ! ${DISTUTILS_USE_PEP517} ]]; then
184 + _distutils-r1_create_setup_cfg
185 + fi
186
187 local setup_py=( setup.py )
188 if [[ ${DISTUTILS_USE_SETUPTOOLS} == pyproject.toml ]]; then
189 @@ -487,7 +554,7 @@ esetup.py() {
190 "${@}" || die -n
191 local ret=${?}
192
193 - if [[ ${BUILD_DIR} ]]; then
194 + if [[ ${BUILD_DIR} && ! ${DISTUTILS_USE_PEP517} ]]; then
195 rm "${HOME}"/.pydistutils.cfg || die -n
196 fi
197
198 @@ -525,9 +592,17 @@ esetup.py() {
199 #
200 # Please note that in order to test the solution properly you need
201 # to unmerge the package first.
202 +#
203 +# This function is not available in PEP517 mode. The eclass provides
204 +# a venv-style install unconditionally therefore, and therefore it
205 +# should no longer be necessary.
206 distutils_install_for_testing() {
207 debug-print-function ${FUNCNAME} "${@}"
208
209 + if [[ ${DISTUTILS_USE_PEP517} ]]; then
210 + die "${FUNCNAME} is not implemented in PEP517 mode"
211 + fi
212 +
213 # A few notes about --via-home mode:
214 # 1) 'install --home' is terribly broken on pypy, so we need
215 # to override --install-lib and --install-scripts,
216 @@ -676,8 +751,10 @@ distutils-r1_python_prepare_all() {
217 fi
218 fi
219
220 - _distutils-r1_disable_ez_setup
221 - _distutils-r1_handle_pyproject_toml
222 + if [[ ! ${DISTUTILS_USE_PEP517} ]]; then
223 + _distutils-r1_disable_ez_setup
224 + _distutils-r1_handle_pyproject_toml
225 + fi
226
227 if [[ ${DISTUTILS_IN_SOURCE_BUILD} && ! ${DISTUTILS_SINGLE_IMPL} ]]
228 then
229 @@ -721,8 +798,8 @@ _distutils-r1_create_setup_cfg() {
230 zip_safe = False
231 _EOF_
232
233 - # we can't refer to ${D} before src_install()
234 if [[ ${EBUILD_PHASE} == install ]]; then
235 + # we can't refer to ${D} before src_install()
236 cat >> "${HOME}"/.pydistutils.cfg <<-_EOF_ || die
237
238 # installation paths -- allow calling extra install targets
239 @@ -734,6 +811,7 @@ _distutils-r1_create_setup_cfg() {
240 _EOF_
241
242 if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
243 + # this gets appended to [install]
244 cat >> "${HOME}"/.pydistutils.cfg <<-_EOF_ || die
245 install_scripts = $(python_get_scriptdir)
246 _EOF_
247 @@ -753,6 +831,35 @@ _distutils-r1_copy_egg_info() {
248 find -name '*.egg-info' -type d -exec cp -R -p {} "${BUILD_DIR}"/ ';' || die
249 }
250
251 +# @FUNCTION: _distutils-r1_backend_to_key
252 +# @USAGE: <backend>
253 +# @INTERNAL
254 +# @DESCRIPTION:
255 +# Print the DISTUTILS_USE_PEP517 value corresponding to the backend
256 +# passed as the only argument.
257 +_distutils-r1_backend_to_key() {
258 + debug-print-function ${FUNCNAME} "${@}"
259 +
260 + local backend=${1}
261 + case ${backend} in
262 + flit_core.buildapi)
263 + echo flit
264 + ;;
265 + pdm.pep517.api)
266 + echo pdm
267 + ;;
268 + poetry.core.masonry.api)
269 + echo poetry
270 + ;;
271 + setuptools.build_meta|setuptools.build_meta:__legacy__)
272 + echo setuptools
273 + ;;
274 + *)
275 + die "Unknown backend: ${backend}"
276 + ;;
277 + esac
278 +}
279 +
280 # @FUNCTION: distutils-r1_python_compile
281 # @USAGE: [additional-args...]
282 # @DESCRIPTION:
283 @@ -767,16 +874,88 @@ distutils-r1_python_compile() {
284
285 _python_check_EPYTHON
286
287 - _distutils-r1_copy_egg_info
288 + # call setup.py build when using setuptools (either via PEP517
289 + # or in legacy mode)
290 + if [[ ${DISTUTILS_USE_PEP517:-setuptools} == setuptools ]]; then
291 + _distutils-r1_copy_egg_info
292
293 - # distutils is parallel-capable since py3.5
294 - local jobs=$(makeopts_jobs "${MAKEOPTS}" INF)
295 - if [[ ${jobs} == INF ]]; then
296 - local nproc=$(get_nproc)
297 - jobs=$(( nproc + 1 ))
298 + # distutils is parallel-capable since py3.5
299 + local jobs=$(makeopts_jobs "${MAKEOPTS}" INF)
300 + if [[ ${jobs} == INF ]]; then
301 + local nproc=$(get_nproc)
302 + jobs=$(( nproc + 1 ))
303 + fi
304 +
305 + esetup.py build -j "${jobs}" "${@}"
306 fi
307
308 - esetup.py build -j "${jobs}" "${@}"
309 + if [[ ${DISTUTILS_USE_PEP517} ]]; then
310 + if [[ -n ${DISTUTILS_ARGS[@]} || -n ${mydistutilsargs[@]} ]]; then
311 + die "DISTUTILS_ARGS are not supported in PEP-517 mode"
312 + fi
313 +
314 + # python likes to compile any module it sees, which triggers sandbox
315 + # failures if some packages haven't compiled their modules yet.
316 + addpredict "${EPREFIX}/usr/lib/${EPYTHON}"
317 + addpredict /usr/lib/pypy3.8
318 + addpredict /usr/lib/portage/pym
319 + addpredict /usr/local # bug 498232
320 +
321 + local -x WHEEL_BUILD_DIR=${BUILD_DIR}/wheel
322 + mkdir -p "${WHEEL_BUILD_DIR}"
323 +
324 + local build_backend
325 + if [[ -f pyproject.toml ]]; then
326 + build_backend=$("${EPYTHON}" -c 'import tomli; \
327 + print(tomli.load(open("pyproject.toml", "rb")) \
328 + ["build-system"]["build-backend"])' 2>/dev/null)
329 + fi
330 + if [[ -z ${build_backend} && ${DISTUTILS_USE_PEP517} == setuptools &&
331 + -f setup.py ]]
332 + then
333 + # use the legacy setuptools backend
334 + build_backend=setuptools.build_meta:__legacy__
335 + fi
336 + [[ -z ${build_backend} ]] &&
337 + die "Unable to obtain build-backend from pyproject.toml"
338 +
339 + if [[ ${DISTUTILS_USE_PEP517} != standalone ]]; then
340 + local expected_value=$(_distutils-r1_backend_to_key "${build_backend}")
341 + if [[ ${DISTUTILS_USE_PEP517} != ${expected_value} ]]; then
342 + eerror "DISTUTILS_USE_PEP517 does not match pyproject.toml!"
343 + eerror " have: DISTUTILS_USE_PEP517=${DISTUTILS_USE_PEP517}"
344 + eerror "expected: DISTUTILS_USE_PEP517=${expected_value}"
345 + eerror "(backend: ${build_backend})"
346 + die "DISTUTILS_USE_PEP517 value incorrect"
347 + fi
348 + fi
349 +
350 + einfo "Building a wheel via ${build_backend}"
351 + "${EPYTHON}" -c "import ${build_backend%:*}; \
352 + import os; \
353 + ${build_backend/:/.}.build_wheel(os.environ['WHEEL_BUILD_DIR'])" ||
354 + die "Wheel build failed"
355 +
356 + local wheel=( "${WHEEL_BUILD_DIR}"/*.whl )
357 + if [[ ${#wheel[@]} -ne 1 ]]; then
358 + die "Incorrect number of wheels created (${#wheel[@]}): ${wheel[*]}"
359 + fi
360 +
361 + local root=${BUILD_DIR}/install
362 + pip install --root "${root}" --no-deps --disable-pip-version-check \
363 + --progress-bar off --verbose --ignore-installed \
364 + --no-warn-script-location "${wheel}" ||
365 + die "pip install failed"
366 +
367 + # enable venv magic inside the install tree
368 + mkdir -p "${root}"/usr/bin || die
369 + ln -s "${PYTHON}" "${root}/usr/bin/${EPYTHON}" || die
370 + ln -s "${EPYTHON}" "${root}/usr/bin/python3" || die
371 + ln -s "${EPYTHON}" "${root}/usr/bin/python" || die
372 + cat > "${root}"/usr/pyvenv.cfg <<-EOF || die
373 + include-system-site-packages = true
374 + EOF
375 + fi
376 }
377
378 # @FUNCTION: _distutils-r1_wrap_scripts
379 @@ -888,59 +1067,74 @@ distutils-r1_python_install() {
380
381 _python_check_EPYTHON
382
383 - local root=${D%/}/_${EPYTHON}
384 - [[ ${DISTUTILS_SINGLE_IMPL} ]] && root=${D%/}
385 -
386 - # inline DISTUTILS_ARGS logic from esetup.py in order to make
387 - # argv overwriting easier
388 - local args=(
389 - "${DISTUTILS_ARGS[@]}"
390 - "${mydistutilsargs[@]}"
391 - install --skip-build --root="${root}" "${args[@]}"
392 - "${@}"
393 - )
394 - local DISTUTILS_ARGS=()
395 - local mydistutilsargs=()
396 + local scriptdir=${EPREFIX}/usr/bin
397 + if [[ ${DISTUTILS_USE_PEP517} ]]; then
398 + local root=${BUILD_DIR}/install
399 + local rscriptdir=${root}$(python_get_scriptdir)
400 + [[ -d ${rscriptdir} ]] &&
401 + die "${rscriptdir} should not exist!"
402 + # remove venv magic
403 + rm "${root}"/usr/{pyvenv.cfg,bin/{python,python3,${EPYTHON}}} || die
404 + find "${root}"/usr/bin -empty -delete || die
405 + if [[ ! ${DISTUTILS_SINGLE_IMPL} && -d ${root}/usr/bin ]]; then
406 + mkdir -p "${rscriptdir%/*}" || die
407 + mv "${root}/usr/bin" "${rscriptdir}" || die
408 + fi
409 + python_optimize "${root}$(python_get_sitedir)"
410 + else
411 + local root=${D%/}/_${EPYTHON}
412 + [[ ${DISTUTILS_SINGLE_IMPL} ]] && root=${D%/}
413 +
414 + # inline DISTUTILS_ARGS logic from esetup.py in order to make
415 + # argv overwriting easier
416 + local args=(
417 + "${DISTUTILS_ARGS[@]}"
418 + "${mydistutilsargs[@]}"
419 + install --skip-build --root="${root}" "${args[@]}"
420 + "${@}"
421 + )
422 + local DISTUTILS_ARGS=()
423 + local mydistutilsargs=()
424
425 - # enable compilation for the install phase.
426 - local -x PYTHONDONTWRITEBYTECODE=
427 + # enable compilation for the install phase.
428 + local -x PYTHONDONTWRITEBYTECODE=
429
430 - # python likes to compile any module it sees, which triggers sandbox
431 - # failures if some packages haven't compiled their modules yet.
432 - addpredict "${EPREFIX}/usr/lib/${EPYTHON}"
433 - addpredict /usr/lib/pypy3.8
434 - addpredict /usr/lib/portage/pym
435 - addpredict /usr/local # bug 498232
436 + # python likes to compile any module it sees, which triggers sandbox
437 + # failures if some packages haven't compiled their modules yet.
438 + addpredict "${EPREFIX}/usr/lib/${EPYTHON}"
439 + addpredict /usr/lib/pypy3.8
440 + addpredict /usr/lib/portage/pym
441 + addpredict /usr/local # bug 498232
442
443 - if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
444 - # user may override --install-scripts
445 - # note: this is poor but distutils argv parsing is dumb
446 - local scriptdir=${EPREFIX}/usr/bin
447 -
448 - # rewrite all the arguments
449 - set -- "${args[@]}"
450 - args=()
451 - while [[ ${@} ]]; do
452 - local a=${1}
453 - shift
454 + if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
455 + # user may override --install-scripts
456 + # note: this is poor but distutils argv parsing is dumb
457 +
458 + # rewrite all the arguments
459 + set -- "${args[@]}"
460 + args=()
461 + while [[ ${@} ]]; do
462 + local a=${1}
463 + shift
464 +
465 + case ${a} in
466 + --install-scripts=*)
467 + scriptdir=${a#--install-scripts=}
468 + ;;
469 + --install-scripts)
470 + scriptdir=${1}
471 + shift
472 + ;;
473 + *)
474 + args+=( "${a}" )
475 + ;;
476 + esac
477 + done
478 + fi
479
480 - case ${a} in
481 - --install-scripts=*)
482 - scriptdir=${a#--install-scripts=}
483 - ;;
484 - --install-scripts)
485 - scriptdir=${1}
486 - shift
487 - ;;
488 - *)
489 - args+=( "${a}" )
490 - ;;
491 - esac
492 - done
493 + esetup.py "${args[@]}"
494 fi
495
496 - esetup.py "${args[@]}"
497 -
498 local forbidden_package_names=(
499 examples test tests
500 .pytest_cache .hypothesis
501 @@ -964,8 +1158,10 @@ distutils-r1_python_install() {
502 die "Package installs 'share' in PyPy prefix, see bug #465546."
503 fi
504
505 - if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
506 + if [[ ! ${DISTUTILS_SINGLE_IMPL} || ${DISTUTILS_USE_PEP517} ]]; then
507 multibuild_merge_root "${root}" "${D%/}"
508 + fi
509 + if [[ ! ${DISTUTILS_SINGLE_IMPL} ]]; then
510 _distutils-r1_wrap_scripts "${scriptdir}"
511 fi
512 }
513 @@ -1006,12 +1202,21 @@ distutils-r1_run_phase() {
514 fi
515 local BUILD_DIR=${BUILD_DIR}/build
516 fi
517 - local -x PYTHONPATH="${BUILD_DIR}/lib:${PYTHONPATH}"
518
519 - # make PATH local for distutils_install_for_testing calls
520 - # it makes little sense to let user modify PATH in per-impl phases
521 - # and _all() already localizes it
522 - local -x PATH=${PATH}
523 + if [[ ${DISTUTILS_USE_PEP517} ]]; then
524 + local -x PATH=${BUILD_DIR}/install/usr/bin:${PATH}
525 + else
526 + local -x PYTHONPATH="${BUILD_DIR}/lib:${PYTHONPATH}"
527 +
528 + # make PATH local for distutils_install_for_testing calls
529 + # it makes little sense to let user modify PATH in per-impl phases
530 + # and _all() already localizes it
531 + local -x PATH=${PATH}
532 +
533 + # Undo the default switch in setuptools-60+ for the time being,
534 + # to avoid replacing .egg-info file with directory in-place.
535 + local -x SETUPTOOLS_USE_DISTUTILS="${SETUPTOOLS_USE_DISTUTILS:-stdlib}"
536 + fi
537
538 # Bug 559644
539 # using PYTHONPATH when the ${BUILD_DIR}/lib is not created yet might lead to
540 @@ -1037,10 +1242,6 @@ distutils-r1_run_phase() {
541
542 local -x LDSHARED="${CC} ${ldopts}" LDCXXSHARED="${CXX} ${ldopts}"
543
544 - # Undo the default switch in setuptools-60+ for the time being,
545 - # to avoid replacing .egg-info file with directory in-place.
546 - local -x SETUPTOOLS_USE_DISTUTILS="${SETUPTOOLS_USE_DISTUTILS:-stdlib}"
547 -
548 "${@}"
549
550 cd "${_DISTUTILS_INITIAL_CWD}" || die
551 --
552 2.34.1