partis-pyproj 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/PKG-INFO +52 -19
  2. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/README.md +46 -14
  3. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/pyproject.toml +3 -2
  4. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/_nonprintable.py +4 -2
  5. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/__init__.py +1 -0
  6. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/builder.py +39 -6
  7. partis_pyproj-0.1.7/src/pyproj/builder/download.py +189 -0
  8. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/load_module.py +11 -9
  9. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/pptoml.py +2 -1
  10. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/pyproj.py +8 -0
  11. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/template.py +21 -8
  12. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/LICENSE.txt +0 -0
  13. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/__init__.py +0 -0
  14. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/_legacy_setup.py +0 -0
  15. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/backend.py +0 -0
  16. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/cargo.py +0 -0
  17. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/cmake.py +0 -0
  18. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/meson.py +0 -0
  19. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/builder/process.py +0 -0
  20. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/__init__.py +0 -0
  21. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_base.py +0 -0
  22. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_binary.py +0 -0
  23. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_copy.py +0 -0
  24. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_source.py +0 -0
  25. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_targz.py +0 -0
  26. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/dist_file/dist_zip.py +0 -0
  27. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/file.py +0 -0
  28. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/legacy.py +0 -0
  29. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/norms.py +0 -0
  30. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/path/__init__.py +0 -0
  31. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/path/match.py +0 -0
  32. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/path/pattern.py +0 -0
  33. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/path/utils.py +0 -0
  34. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/pep.py +0 -0
  35. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/pkginfo.py +0 -0
  36. {partis_pyproj-0.1.5 → partis_pyproj-0.1.7}/src/pyproj/validate.py +0 -0
@@ -1,22 +1,23 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: partis-pyproj
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Requires-Python: >=3.8
5
5
  Author-email: "Nanohmics Inc." <software.support@nanohmics.com>
6
6
  Maintainer-email: "Nanohmics Inc." <software.support@nanohmics.com>
7
7
  Summary: Minimal set of Python project utilities (PEP-517/621)
8
8
  License-File: LICENSE.txt
9
- Classifier: Topic :: Software Development :: Build Tools
9
+ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: License :: OSI Approved :: BSD License
11
13
  Classifier: Operating System :: Microsoft :: Windows
12
- Classifier: Programming Language :: Python
13
- Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Provides-Extra: meson
18
18
  Provides-Extra: cmake
19
19
  Requires-Dist: packaging>=24.2
20
+ Requires-Dist: requests>=2.32.3
20
21
  Requires-Dist: tomli>=2.0.1
21
22
  Requires-Dist: meson>=0.61.3; extra == "meson"
22
23
  Requires-Dist: ninja>=1.10.2.3; extra == "meson"
@@ -278,6 +279,7 @@ setup_args: array{STRING} # 3-stage build
278
279
  compile_args: array{STRING} # 3-stage build
279
280
  install_args: array{STRING} # 3-stage build
280
281
  options: table{STRING|BOOL}? # options passed to builder from pyproject.toml
282
+ env: table{STRING|STRING}? # environment variables to set
281
283
  build_clean: BOOL? # control cleanup (ie for development builds)
282
284
  enabled: (BOOL|MARKER)? # environment marker
283
285
  ```
@@ -287,11 +289,28 @@ There are several entry points available as-is:
287
289
  - `partis.pyproj.builder:meson` - Support for [Meson Build system](https://mesonbuild.com/) with the 'extra' ``partis-pyproj[meson]``
288
290
  - `partis.pyproj.builder:cmake` - Support for [CMake](https://cmake.org/) with the 'extra' ``partis-pyproj[cmake]``
289
291
  - `partis.pyproj.builder:process` - Support for running arbitrary command line executable
292
+ - `partis.pyproj.builder:download` - Support for downloading a file to `build_dir`
293
+
294
+
295
+ Options for `partis.pyproj.builder:download`:
296
+
297
+ ```
298
+ [tool.pyproj.targets.options]
299
+ url: URL
300
+ checksum: ALG=HEX # expected checksum
301
+ filename: STRING? # rename in build_dir, defaults to mangled version of url
302
+ extract: BOOL? # extract/decompress as a tar file
303
+ executable: BOOL? # set execute permission
304
+ ```
305
+
306
+ Checksum `ALG` can be `sha256`, `md5`, or another algorithm in [hashlib](https://docs.python.org/3/library/hashlib.html)
307
+
308
+ **Example**
290
309
 
291
310
  In this example, the source directory must contain appropriate `meson.build` files,
292
311
  since the 'pyproject.toml' configuration only provides a way of running
293
312
  ``meson setup`` and ``meson compile``.
294
- For example:
313
+
295
314
 
296
315
  ```toml
297
316
  # pyproject.toml
@@ -358,7 +377,7 @@ Python [Template string](https://docs.python.org/3/library/string.html#template-
358
377
  - `$$` is an escape; it is replaced with a single `$`.
359
378
  - `${identifier}` names a substitution placeholder matching a mapping key of "identifier".
360
379
 
361
- However, `$identifier` (without braces) is not supported, instead allowing more expressive substitutions.
380
+ However, `$identifier` (without braces) is *not supported*, but this restriction allows more expressive substitutions.
362
381
 
363
382
  ```
364
383
  substitution: "${" (variable|literal|SEP)+ "}"
@@ -367,16 +386,30 @@ SEP: "/"
367
386
  literal: "'" CHAR+ "'"
368
387
  IDENTIFIER: < python identifier >
369
388
  INTEGER: < integer >
370
- CHAR: < ascii letter, number (not leading), or underscore >
389
+ CHAR: < ascii alpha-numeric, dot ".", dash "-", underscore "_" >
371
390
  ```
372
391
 
373
- Variable names can reference most of the content of the original 'pyproject.toml',
374
- as well as values already substituted in the build target or earlier targets.
392
+ Top-level template variable identifiers can reference the content of the original 'pyproject.toml', config. settings, environment variables, and values already substituted in the build target or earlier targets.
375
393
  If the substitution contains any separators the result is interpreted as a path, converted to platform-specific filesystem format, and resolved to project directory.
376
-
377
- **Example**
378
-
379
- The value of `options.some_option` in the example below would be substituted with a filesystem equivalent path for `{root}/build/something/my_pkg/xyz/abc.so`:
394
+ The template namespace contains the following keys:
395
+
396
+ - `root`: Absolute path to project root directory
397
+ - `tmpdir`: A temporary directory created and shared by all build targets.
398
+ This directory is removed before the distribution is created, so any needed files must be copied back to a location within the project tree by one of the targets
399
+ (eg. the "install" step of 3-stage builds with a `prefix` within the project).
400
+ - `pptoml`: Top-level of parsed `pyproject.toml`
401
+ - `project`: The `project` section, including `name`, `version`, etc.
402
+ - `pyproj`: The `tool.pyproj` section.
403
+ - `config_settings`: A mapping from the `config_settings` passed to backend per PEP-517
404
+ after defaults applied from `tool.pyproj.config` (described below).
405
+ - `targets`': List from `tool.pyproj.targets`, updated as targets are processed.
406
+ - `work_dir`, `src_dir`, `build_dir`, `prefix`: Per-target values (if processed before the substitution)
407
+ - `env`: Defaults to `os.environ`, or per-target value from `tool.pyproj.targets.env`
408
+ (if processed before the substitution).
409
+ - `options`: Per-target value from `tool.pyproj.targets.options` (if processed before the substitution)
410
+
411
+ Template substitutions are processed (once) in the *order in which they appear* from the `pyproject.toml`, no static analysis is performed. It is up to the developer to put them in the needed order if one template references a value resulting from another template.
412
+ In the example below, the value of `options.some_option` would be substituted with a filesystem equivalent path for `{root}/build/something/my_pkg/xyz/abc.so`:
380
413
 
381
414
 
382
415
  ```toml
@@ -397,14 +430,14 @@ location according to a local installation scheme
397
430
  these can be specified within sub-tables.
398
431
  Available install scheme keys, and **example** corresponding install locations, are:
399
432
 
400
- - `purelib` ("pure" library Python path): ``{prefix}/lib/python{X}.{Y}/site-packages/``
401
- - `platlib` (platform specific Python path): ``{prefix}/lib{platform}/python{X}.{Y}/site-packages/``
433
+ - `purelib` ("pure" library Python path): ``{venv}/lib/python{X}.{Y}/site-packages/``
434
+ - `platlib` (platform specific Python path): ``{venv}/lib{platform}/python{X}.{Y}/site-packages/``
402
435
  Both `purelib` and `platlib` install to the base 'site-packages'
403
436
  directory, so any files copied to these paths should be placed within a
404
437
  desired top-level package directory.
405
438
 
406
- - `headers` (INCLUDE search paths): ``{prefix}/include/{site}/python{X}.{Y}{abiflags}/{distname}/``
407
- - `scripts` (executable search path): ``{prefix}/bin/``
439
+ - `headers` (INCLUDE search paths): ``{venv}/include/{site}/python{X}.{Y}{abiflags}/{distname}/``
440
+ - `scripts` (executable search path): ``{venv}/bin/``
408
441
  Even though any files added to the `scripts` path will be installed to
409
442
  the `bin` directory, there is often an issue with the 'execute' permission
410
443
  being set correctly by the installer (e.g. `pip`).
@@ -412,7 +445,7 @@ Available install scheme keys, and **example** corresponding install locations,
412
445
  use the ``[project.scripts]`` section to add an entry point that will then
413
446
  run the desired executable as a sub-process.
414
447
 
415
- - `data` (generic data path): ``{prefix}/``
448
+ - `data` (generic data path): ``{venv}/``
416
449
 
417
450
  ```toml
418
451
  # pyproject.toml
@@ -252,6 +252,7 @@ setup_args: array{STRING} # 3-stage build
252
252
  compile_args: array{STRING} # 3-stage build
253
253
  install_args: array{STRING} # 3-stage build
254
254
  options: table{STRING|BOOL}? # options passed to builder from pyproject.toml
255
+ env: table{STRING|STRING}? # environment variables to set
255
256
  build_clean: BOOL? # control cleanup (ie for development builds)
256
257
  enabled: (BOOL|MARKER)? # environment marker
257
258
  ```
@@ -261,11 +262,28 @@ There are several entry points available as-is:
261
262
  - `partis.pyproj.builder:meson` - Support for [Meson Build system](https://mesonbuild.com/) with the 'extra' ``partis-pyproj[meson]``
262
263
  - `partis.pyproj.builder:cmake` - Support for [CMake](https://cmake.org/) with the 'extra' ``partis-pyproj[cmake]``
263
264
  - `partis.pyproj.builder:process` - Support for running arbitrary command line executable
265
+ - `partis.pyproj.builder:download` - Support for downloading a file to `build_dir`
266
+
267
+
268
+ Options for `partis.pyproj.builder:download`:
269
+
270
+ ```
271
+ [tool.pyproj.targets.options]
272
+ url: URL
273
+ checksum: ALG=HEX # expected checksum
274
+ filename: STRING? # rename in build_dir, defaults to mangled version of url
275
+ extract: BOOL? # extract/decompress as a tar file
276
+ executable: BOOL? # set execute permission
277
+ ```
278
+
279
+ Checksum `ALG` can be `sha256`, `md5`, or another algorithm in [hashlib](https://docs.python.org/3/library/hashlib.html)
280
+
281
+ **Example**
264
282
 
265
283
  In this example, the source directory must contain appropriate `meson.build` files,
266
284
  since the 'pyproject.toml' configuration only provides a way of running
267
285
  ``meson setup`` and ``meson compile``.
268
- For example:
286
+
269
287
 
270
288
  ```toml
271
289
  # pyproject.toml
@@ -332,7 +350,7 @@ Python [Template string](https://docs.python.org/3/library/string.html#template-
332
350
  - `$$` is an escape; it is replaced with a single `$`.
333
351
  - `${identifier}` names a substitution placeholder matching a mapping key of "identifier".
334
352
 
335
- However, `$identifier` (without braces) is not supported, instead allowing more expressive substitutions.
353
+ However, `$identifier` (without braces) is *not supported*, but this restriction allows more expressive substitutions.
336
354
 
337
355
  ```
338
356
  substitution: "${" (variable|literal|SEP)+ "}"
@@ -341,16 +359,30 @@ SEP: "/"
341
359
  literal: "'" CHAR+ "'"
342
360
  IDENTIFIER: < python identifier >
343
361
  INTEGER: < integer >
344
- CHAR: < ascii letter, number (not leading), or underscore >
362
+ CHAR: < ascii alpha-numeric, dot ".", dash "-", underscore "_" >
345
363
  ```
346
364
 
347
- Variable names can reference most of the content of the original 'pyproject.toml',
348
- as well as values already substituted in the build target or earlier targets.
365
+ Top-level template variable identifiers can reference the content of the original 'pyproject.toml', config. settings, environment variables, and values already substituted in the build target or earlier targets.
349
366
  If the substitution contains any separators the result is interpreted as a path, converted to platform-specific filesystem format, and resolved to project directory.
350
-
351
- **Example**
352
-
353
- The value of `options.some_option` in the example below would be substituted with a filesystem equivalent path for `{root}/build/something/my_pkg/xyz/abc.so`:
367
+ The template namespace contains the following keys:
368
+
369
+ - `root`: Absolute path to project root directory
370
+ - `tmpdir`: A temporary directory created and shared by all build targets.
371
+ This directory is removed before the distribution is created, so any needed files must be copied back to a location within the project tree by one of the targets
372
+ (eg. the "install" step of 3-stage builds with a `prefix` within the project).
373
+ - `pptoml`: Top-level of parsed `pyproject.toml`
374
+ - `project`: The `project` section, including `name`, `version`, etc.
375
+ - `pyproj`: The `tool.pyproj` section.
376
+ - `config_settings`: A mapping from the `config_settings` passed to backend per PEP-517
377
+ after defaults applied from `tool.pyproj.config` (described below).
378
+ - `targets`': List from `tool.pyproj.targets`, updated as targets are processed.
379
+ - `work_dir`, `src_dir`, `build_dir`, `prefix`: Per-target values (if processed before the substitution)
380
+ - `env`: Defaults to `os.environ`, or per-target value from `tool.pyproj.targets.env`
381
+ (if processed before the substitution).
382
+ - `options`: Per-target value from `tool.pyproj.targets.options` (if processed before the substitution)
383
+
384
+ Template substitutions are processed (once) in the *order in which they appear* from the `pyproject.toml`, no static analysis is performed. It is up to the developer to put them in the needed order if one template references a value resulting from another template.
385
+ In the example below, the value of `options.some_option` would be substituted with a filesystem equivalent path for `{root}/build/something/my_pkg/xyz/abc.so`:
354
386
 
355
387
 
356
388
  ```toml
@@ -371,14 +403,14 @@ location according to a local installation scheme
371
403
  these can be specified within sub-tables.
372
404
  Available install scheme keys, and **example** corresponding install locations, are:
373
405
 
374
- - `purelib` ("pure" library Python path): ``{prefix}/lib/python{X}.{Y}/site-packages/``
375
- - `platlib` (platform specific Python path): ``{prefix}/lib{platform}/python{X}.{Y}/site-packages/``
406
+ - `purelib` ("pure" library Python path): ``{venv}/lib/python{X}.{Y}/site-packages/``
407
+ - `platlib` (platform specific Python path): ``{venv}/lib{platform}/python{X}.{Y}/site-packages/``
376
408
  Both `purelib` and `platlib` install to the base 'site-packages'
377
409
  directory, so any files copied to these paths should be placed within a
378
410
  desired top-level package directory.
379
411
 
380
- - `headers` (INCLUDE search paths): ``{prefix}/include/{site}/python{X}.{Y}{abiflags}/{distname}/``
381
- - `scripts` (executable search path): ``{prefix}/bin/``
412
+ - `headers` (INCLUDE search paths): ``{venv}/include/{site}/python{X}.{Y}{abiflags}/{distname}/``
413
+ - `scripts` (executable search path): ``{venv}/bin/``
382
414
  Even though any files added to the `scripts` path will be installed to
383
415
  the `bin` directory, there is often an issue with the 'execute' permission
384
416
  being set correctly by the installer (e.g. `pip`).
@@ -386,7 +418,7 @@ Available install scheme keys, and **example** corresponding install locations,
386
418
  use the ``[project.scripts]`` section to add an entry point that will then
387
419
  run the desired executable as a sub-process.
388
420
 
389
- - `data` (generic data path): ``{prefix}/``
421
+ - `data` (generic data path): ``{venv}/``
390
422
 
391
423
  ```toml
392
424
  # pyproject.toml
@@ -26,7 +26,7 @@
26
26
 
27
27
  [project]
28
28
  name = "partis-pyproj"
29
- version = "0.1.5"
29
+ version = "0.1.7"
30
30
  description = "Minimal set of Python project utilities (PEP-517/621)"
31
31
  maintainers = [
32
32
  { name = "Nanohmics Inc.", email = "software.support@nanohmics.com" } ]
@@ -45,7 +45,8 @@ classifiers = [
45
45
  requires-python = ">= 3.8"
46
46
  dependencies = [
47
47
  "packaging >= 24.2",
48
- "tomli >= 2.0.1" ]
48
+ "tomli >= 2.0.1",
49
+ "requests >= 2.32.3"]
49
50
 
50
51
  #===============================================================================
51
52
  [project.optional-dependencies]
@@ -4,13 +4,13 @@ import re
4
4
 
5
5
  #===============================================================================
6
6
  def _gen_nonprintable():
7
- test = ''
7
+ test = []
8
8
 
9
9
  ns = [ [0,], ]
10
10
 
11
11
  for i in range(1, sys.maxunicode+1):
12
12
  c = chr(i)
13
- test += c
13
+ test.append(c)
14
14
 
15
15
  if not ( c.isprintable() or c in '\n\t' ):
16
16
  n = ns[-1]
@@ -23,6 +23,8 @@ def _gen_nonprintable():
23
23
  else:
24
24
  ns.append([i,])
25
25
 
26
+ test = ''.join(test)
27
+
26
28
  # print(len(test), test.isprintable())
27
29
  # print(len(ns))
28
30
  # print(ns)
@@ -1,6 +1,7 @@
1
1
 
2
2
  from .builder import Builder
3
3
  from .process import process
4
+ from .download import download
4
5
  from .meson import meson
5
6
  from .cmake import cmake
6
7
  from .cargo import cargo
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import os
3
3
  import os.path as osp
4
- import sys
4
+ import tempfile
5
5
  import sysconfig
6
6
  import re
7
7
  from copy import copy
@@ -61,6 +61,7 @@ class Builder:
61
61
  self.targets = [copy(v) for v in targets]
62
62
  self.clean_dirs = [False]*len(self.targets)
63
63
  self.logger = logger
64
+ self.tmpdir = Path(tempfile.mkdtemp(prefix=f"build-{pyproj.project.name}-"))
64
65
  self.namespace = Namespace({
65
66
  'root': root,
66
67
  'pptoml': pyproj.pptoml,
@@ -69,8 +70,11 @@ class Builder:
69
70
  'config_settings': pyproj.config_settings,
70
71
  'targets': targets,
71
72
  'env': os.environ,
73
+ 'tmpdir': self.tmpdir,
72
74
  'config_vars': sysconfig.get_config_vars()},
73
- root=root)
75
+ root=root,
76
+ # better way for builders to whitelist templated directories?
77
+ dirs=[self.tmpdir, Path(tempfile.gettempdir())/'partis-pyproj-downloads'])
74
78
 
75
79
  #-----------------------------------------------------------------------------
76
80
  def __enter__(self):
@@ -85,11 +89,37 @@ class Builder:
85
89
 
86
90
  #-----------------------------------------------------------------------------
87
91
  def build_targets(self):
92
+ exclusive = {
93
+ target.exclusive: None
94
+ for target in self.targets
95
+ if target.exclusive}
96
+
97
+ if exclusive:
98
+ for i, target in enumerate(self.targets):
99
+ if not (group := target.exclusive):
100
+ continue
101
+
102
+ cur = exclusive[group]
103
+
104
+ if target.enabled:
105
+ if cur is None:
106
+ exclusive[group] = i
107
+
108
+
109
+ missing = [group for group, idx in exclusive.items() if idx is None]
110
+
111
+ if missing:
112
+ raise ValidationError(f"Exclusive group {missing} does not have an enabled target")
113
+
88
114
  for i, target in enumerate(self.targets):
89
115
  if not target.enabled:
90
116
  self.logger.info(f"Skipping targets[{i}], disabled for environment markers")
91
117
  continue
92
118
 
119
+ if (group := target.exclusive) and (group_idx := exclusive.get(group)) != i:
120
+ self.logger.warning(
121
+ f"Skipping targets[{i}], exclusive group {group!r} already satisfied by targets[{group_idx}]")
122
+
93
123
  # each target isolated (shallow) changes to namespace
94
124
  namespace = copy(self.namespace)
95
125
 
@@ -106,9 +136,9 @@ class Builder:
106
136
 
107
137
  abs_path = resolve(abs_path)
108
138
 
109
- if not subdir(self.root, abs_path, check=False):
139
+ if not (subdir(self.root, abs_path, check=False) or subdir(self.tmpdir, abs_path, check=False)):
110
140
  raise FileOutsideRootError(
111
- f"Must be within project root directory:"
141
+ f"Must be within project root directory or tmpdir:"
112
142
  f"file = \"{abs_path}\", root = \"{self.root}\"")
113
143
 
114
144
  if k in ('build_dir', 'prefix') and subdir(abs_path, self.root, check=False):
@@ -249,6 +279,8 @@ class Builder:
249
279
  self.logger.info(f"Removing build dir: {build_dir}")
250
280
  shutil.rmtree(build_dir)
251
281
 
282
+ shutil.rmtree(self.tmpdir)
283
+
252
284
  #===============================================================================
253
285
  class ProcessRunner:
254
286
  #-----------------------------------------------------------------------------
@@ -273,10 +305,11 @@ class ProcessRunner:
273
305
  cmd_exec_src = shutil.which(cmd_exec)
274
306
 
275
307
  if cmd_exec_src is None:
276
- raise ValueError(
308
+ raise ValidationError(
277
309
  f"Executable does not exist or has in-sufficient permissions: {cmd_exec}")
278
310
 
279
- cmd_exec_src = resolve(Path(cmd_exec_src))
311
+ # cmd_exec_src = resolve(Path(cmd_exec_src))
312
+ cmd_exec_src = Path(cmd_exec_src)
280
313
  cmd_name = cmd_exec_src.name
281
314
  args = [str(cmd_exec_src)]+args[1:]
282
315
 
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+ import sys
3
+ import os
4
+ import platform
5
+ import stat
6
+ import re
7
+ from pathlib import Path
8
+ import hashlib
9
+ from urllib.parse import urlsplit
10
+ import tarfile
11
+ import tempfile
12
+ from base64 import urlsafe_b64encode
13
+ import logging
14
+ from .builder import (
15
+ ProcessRunner)
16
+ from ..validate import (
17
+ ValidationError)
18
+ from ..norms import b64_nopad, nonempty_str
19
+
20
+ # replace runs of non-alphanumeric, dot, dash, or underscore
21
+ _filename_subs = re.compile(r'[^a-z0-9\.\-\_]+', re.I)
22
+
23
+ #===============================================================================
24
+ def download(
25
+ pyproj,
26
+ logger: logging.Logger,
27
+ options: dict,
28
+ work_dir: Path,
29
+ src_dir: Path,
30
+ build_dir: Path,
31
+ prefix: Path,
32
+ setup_args: list[str],
33
+ compile_args: list[str],
34
+ install_args: list[str],
35
+ build_clean: bool,
36
+ runner: ProcessRunner):
37
+ """Download a file
38
+ """
39
+ import requests
40
+
41
+ chunk_size = int(options.get('chunk_size', 2**16))
42
+
43
+ url = options.get('url')
44
+ executable = options.get('executable')
45
+
46
+ if not url:
47
+ raise ValidationError(
48
+ "Download 'url' required")
49
+
50
+ url = nonempty_str(url)
51
+
52
+ checksum = options.get('checksum')
53
+
54
+ if checksum is None:
55
+ raise ValidationError(
56
+ "Download 'checksum' required, or explicitly set 'checksum=false'")
57
+
58
+
59
+ filename = options.get('filename', url.split('/')[-1])
60
+ extract = options.get('extract', None)
61
+
62
+ cache_file = _cached_download(url, checksum)
63
+ out_file = build_dir/filename
64
+
65
+ if cache_file.exists():
66
+ logger.info(f"Using cache file: {cache_file}")
67
+
68
+ else:
69
+ # name unique to host/process as countermeasure for race condition
70
+ hostname = re.sub(r'[^a-zA-Z0-9]+', '_', str(platform.node()))
71
+ tmp_name = f"{cache_file.name}-{hostname}-{os.getpid():06d}.tmp"
72
+ tmp_file = cache_file.with_name(tmp_name)
73
+
74
+ if tmp_file.exists():
75
+ tmp_file.unlink()
76
+
77
+ if checksum:
78
+ checksum = checksum.lower()
79
+ alg, _, checksum = checksum.partition('=')
80
+
81
+ try:
82
+ hash = getattr(hashlib, alg)()
83
+
84
+ except AttributeError:
85
+ raise ValidationError(
86
+ f"Checksum algorithm must be one of {hashlib.algorithms_available}: got {alg}") from None
87
+
88
+ else:
89
+ hash = None
90
+
91
+ size = 0
92
+ last_size = 0
93
+
94
+ try:
95
+ logger.info(f"- downloading: {url} -> {tmp_file}")
96
+
97
+ with requests.get(url, stream=True) as req, tmp_file.open('wb') as fp:
98
+ for chunk in req.iter_content(chunk_size=chunk_size):
99
+ if chunk:
100
+ fp.write(chunk)
101
+ size += len(chunk)
102
+
103
+ if hash:
104
+ hash.update(chunk)
105
+
106
+ if size - last_size > 50e6:
107
+ logger.info(f"- {size/1e6:,.1f} MB")
108
+ last_size = size
109
+
110
+ logger.info(f"- complete {size/1e6:,.1f} MB")
111
+
112
+ if hash:
113
+ digest = hash.digest()
114
+
115
+ if checksum.endswith('='):
116
+ digest = urlsafe_b64encode(digest).decode("ascii")
117
+ elif checksum.startswith('x'):
118
+ digest = 'x'+digest.hex()
119
+ else:
120
+ digest = digest.hex()
121
+
122
+ checksum_ok = checksum == digest
123
+ logger.info(f"- checksum{' (OK)' if checksum_ok else ''}: {alg}={digest}")
124
+
125
+ if not checksum_ok:
126
+ raise ValidationError(f"Download checksum did not match: {digest} != {checksum}")
127
+
128
+ except Exception:
129
+ if tmp_file.exists():
130
+ tmp_file.unlink()
131
+
132
+ raise
133
+
134
+ tmp_file.replace(cache_file)
135
+
136
+
137
+ out_file.symlink_to(cache_file)
138
+
139
+ if extract:
140
+ logger.info(f"- extracting: {cache_file} -> {build_dir}")
141
+ with tarfile.open(cache_file, 'r:*') as fp:
142
+ if sys.version_info >= (3, 12):
143
+ # 'filter' argument added, controls behavior of extract
144
+ fp.extractall(
145
+ path=build_dir,
146
+ members=None,
147
+ numeric_owner=False,
148
+ filter='tar')
149
+ else:
150
+ fp.extractall(
151
+ path=build_dir,
152
+ members=None,
153
+ numeric_owner=False)
154
+
155
+ if executable:
156
+ logger.info("- setting executable permission")
157
+ out_file.chmod(out_file.stat().st_mode|stat.S_IXUSR)
158
+
159
+ #===============================================================================
160
+ def _cached_download(url: str, checksum: str) -> Path:
161
+ if not checksum:
162
+ checksum = '0'
163
+
164
+ cache_dir = Path(tempfile.gettempdir())/'partis-pyproj-downloads'
165
+ name = url.split('/')[-1]
166
+ _url = url
167
+
168
+ # hash of url + checksum used to prevent filename collision after url is sanitized
169
+ h = hashlib.sha256()
170
+ h.update(url.encode('utf-8') + checksum.encode('utf-8'))
171
+ # keep only 4 bytes (8 hex characters) worth of the hash
172
+ short = h.digest()[:4].hex()
173
+
174
+ if name != _url:
175
+ # possible for url without a path segment?
176
+ _url = _url.removesuffix('/'+name)
177
+
178
+ url_dirname = _filename_subs.sub('_', _url)
179
+ url_filename = f"{short}-" + _filename_subs.sub('_', name)
180
+
181
+ url_dir = cache_dir/url_dirname
182
+ url_dir.mkdir(exist_ok=True, parents=True)
183
+
184
+ file = url_dir/url_filename
185
+
186
+ info_file = file.with_name(file.name+'.info')
187
+ info_file.write_text(f"{url}\n{checksum}")
188
+
189
+ return file
@@ -24,7 +24,7 @@ from .validate import (
24
24
  mapget )
25
25
 
26
26
  #===============================================================================
27
- class EntryPointError(ValueError):
27
+ class EntryPointError(ValidationError):
28
28
  pass
29
29
 
30
30
  #===============================================================================
@@ -177,16 +177,18 @@ class EntryPoint:
177
177
 
178
178
  cwd = os.getcwd()
179
179
 
180
- try:
181
-
182
- with validating( file = f"{self.name} -> {self.entry}" ):
180
+ with validating( file = f"{self.name} -> {self.entry}" ):
181
+ try:
183
182
  self.func(
184
183
  self.pyproj,
185
184
  logger = self.logger,
186
- **kwargs )
185
+ **kwargs)
187
186
 
188
- except Exception as e:
189
- raise EntryPointError(f"failed to run '{self.entry}'") from e
187
+ except ValidationError:
188
+ raise
189
+
190
+ except Exception as e:
191
+ raise EntryPointError(f"failed to run '{self.entry}'") from e
190
192
 
191
- finally:
192
- os.chdir(cwd)
193
+ finally:
194
+ os.chdir(cwd)
@@ -49,7 +49,7 @@ from .pep import (
49
49
  norm_entry_point_ref,
50
50
  norm_dist_keyword,
51
51
  norm_dist_classifier,
52
- norm_dist_url )
52
+ norm_dist_url)
53
53
 
54
54
  #===============================================================================
55
55
  class dynamic(valid_list):
@@ -260,6 +260,7 @@ class pyproj_build_target(valid_dict):
260
260
  ('compile', 'enabled')]
261
261
  default = {
262
262
  'enabled': valid(True, marker_evaluated),
263
+ 'exclusive': valid('', norm_printable),
263
264
  # NOTE: default builder from backward compatibility
264
265
  'entry': valid('partis.pyproj.builder:meson', norm_entry_point_ref),
265
266
  'options': dict,
@@ -201,6 +201,10 @@ class PyProjBase:
201
201
  """
202
202
  return self._config_settings
203
203
 
204
+ #-----------------------------------------------------------------------------
205
+ # alias for backward compatibility
206
+ config = config_settings
207
+
204
208
  #-----------------------------------------------------------------------------
205
209
  @property
206
210
  def targets(self):
@@ -218,6 +222,9 @@ class PyProjBase:
218
222
  These are no longer restricted to meson, but this attribute kept for backward
219
223
  compatability.
220
224
 
225
+ Inplace changes to the returned object are not propagated back to the target
226
+ configuration.
227
+
221
228
  """
222
229
  targets = self._pptoml.tool.pyproj.targets
223
230
 
@@ -227,6 +234,7 @@ class PyProjBase:
227
234
  meson = dict(targets[0])
228
235
  meson.pop('entry')
229
236
  meson.pop('work_dir')
237
+ meson.pop('env')
230
238
  meson['compile'] = meson.pop('enabled')
231
239
  return pyproj_meson(meson)
232
240
 
@@ -71,12 +71,12 @@ class Template:
71
71
  return '$'
72
72
 
73
73
  if m.group('unterminated') is not None:
74
- raise TemplateError(f"Unterminated template substitution: {m.group()}")
74
+ raise TemplateError(f"Unterminated template substitution {m.group()!r}: {self.template!r}")
75
75
 
76
76
  name = m.group('braced').strip()
77
77
 
78
78
  if not _idpattern.fullmatch(name):
79
- raise TemplateError(f"Invalid template substitution: {name}")
79
+ raise TemplateError(f"Invalid template substitution {name!r}: {self.template!r}")
80
80
 
81
81
  return str(namespace[name])
82
82
 
@@ -95,13 +95,21 @@ class Namespace(Mapping):
95
95
  root:
96
96
  If given, absolute path to project root, used to resolve relative paths and ensure
97
97
  any derived paths are within this parent directory.
98
+ dirs:
99
+ Additional white-listed directories to allow paths
98
100
  """
99
- __slots__ = ['data', 'root']
101
+ __slots__ = ['data', 'root', 'dirs']
100
102
 
101
103
  #-----------------------------------------------------------------------------
102
- def __init__(self, data: Mapping, *, root: Path = None):
104
+ def __init__(self, data: Mapping, *, root: Path = None, dirs: list[Path]|None = None):
105
+ if dirs is None:
106
+ dirs = []
107
+ elif isinstance(dirs, Path):
108
+ dirs = [dirs]
109
+
103
110
  self.data = data
104
111
  self.root = root
112
+ self.dirs = dirs
105
113
 
106
114
  #-----------------------------------------------------------------------------
107
115
  def __iter__(self):
@@ -150,7 +158,10 @@ class Namespace(Mapping):
150
158
  # NOTE: ignored if root is a pure path
151
159
  out = resolve(out)
152
160
 
153
- if not subdir(root, out, check = False):
161
+ if any(subdir(v, out, check = False) for v in self.dirs):
162
+ ...
163
+
164
+ elif not subdir(root, out, check = False):
154
165
  raise FileOutsideRootError(
155
166
  f"Must be within project root directory:"
156
167
  f"\n file = \"{out}\"\n root = \"{root}\"")
@@ -163,6 +174,7 @@ class Namespace(Mapping):
163
174
  obj = cls.__new__(cls)
164
175
  obj.data = copy(self.data)
165
176
  obj.root = self.root
177
+ obj.dirs = self.dirs
166
178
  return obj
167
179
 
168
180
  #-----------------------------------------------------------------------------
@@ -210,9 +222,10 @@ def template_substitute(
210
222
  return cls(Template(value).substitute(namespace))
211
223
 
212
224
  if isinstance(value, Path):
213
- return cls(*(
214
- Template(v).substitute(namespace)
215
- for v in value.parts))
225
+ return cls(Template(str(value)).substitute(namespace))
226
+ # return cls(*(
227
+ # Template(v).substitute(namespace)
228
+ # for v in value.parts))
216
229
 
217
230
  if isinstance(value, Mapping):
218
231
  return cls({
File without changes