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