pystand 1.5__py3-none-any.whl → 1.7__py3-none-any.whl
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.
- {pystand-1.5.dist-info → pystand-1.7.dist-info}/METADATA +18 -13
- pystand-1.7.dist-info/RECORD +6 -0
- {pystand-1.5.dist-info → pystand-1.7.dist-info}/WHEEL +1 -1
- pystand.py +78 -42
- pystand-1.5.dist-info/RECORD +0 -6
- {pystand-1.5.dist-info → pystand-1.7.dist-info}/entry_points.txt +0 -0
- {pystand-1.5.dist-info → pystand-1.7.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pystand
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.7
|
4
4
|
Summary: Install Python versions from python-build-standalone project
|
5
5
|
Author-email: Mark Blakeney <mark.blakeney@bullet-systems.net>
|
6
6
|
License: GPLv3
|
@@ -99,12 +99,15 @@ $ pipx install --python $(pystand path -p 3.12) cowsay
|
|
99
99
|
See detailed usage information in the [Usage](#usage) section that
|
100
100
|
follows.
|
101
101
|
|
102
|
-
Note that
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
Note that similar tools such as [`pdm python`][pdmpy], [`hatch
|
103
|
+
python`][hatchpy], and [`rye toolchain`][ryepy] also use
|
104
|
+
[`python-build-standalone`][pbs] build releases. However, `pystand` is
|
105
|
+
unique because it directly checks the [`python-build-standalone`][pbs]
|
106
|
+
github site for new [releases][pbs-rel]. Those other tools
|
107
|
+
require a software update before they can fetch and use new
|
108
|
+
[`python-build-standalone`][pbs] releases. This means that new Python
|
109
|
+
versions and updates are always available more quickly from `pystand`
|
110
|
+
than those other tools.
|
108
111
|
|
109
112
|
This utility has been developed and tested on Linux but should also work
|
110
113
|
on macOS and Windows although has not been tried on those platforms. The
|
@@ -118,8 +121,8 @@ Type `pystand` or `pystand -h` to view the usage summary:
|
|
118
121
|
```
|
119
122
|
usage: pystand [-h] [-D DISTRIBUTION] [-B BASE_DIR] [-C CACHE_MINUTES]
|
120
123
|
[--purge-days PURGE_DAYS]
|
121
|
-
[--github-access-token GITHUB_ACCESS_TOKEN]
|
122
|
-
[--
|
124
|
+
[--github-access-token GITHUB_ACCESS_TOKEN] [--no-strip]
|
125
|
+
[--no-extra-strip] [-V]
|
123
126
|
{install,update,remove,list,show,path} ...
|
124
127
|
|
125
128
|
Command line tool to download, install, and update pre-built Python versions
|
@@ -147,8 +150,8 @@ options:
|
|
147
150
|
--github-access-token GITHUB_ACCESS_TOKEN
|
148
151
|
optional Github access token. Can specify to reduce
|
149
152
|
rate limiting.
|
150
|
-
--
|
151
|
-
|
153
|
+
--no-strip do not use or create stripped binaries
|
154
|
+
--no-extra-strip do not restrip already stripped source binaries
|
152
155
|
-V show pystand version
|
153
156
|
|
154
157
|
Commands:
|
@@ -342,8 +345,9 @@ anything after on a line) are ignored. Type `pystand` to see all
|
|
342
345
|
supported options.
|
343
346
|
|
344
347
|
The global options: `--distribution`, `--base-dir`, `--cache-minutes`,
|
345
|
-
`--purge-days`, `--github-access-token`, `--
|
346
|
-
sensible candidates to consider setting
|
348
|
+
`--purge-days`, `--github-access-token`, `--no-strip`,
|
349
|
+
`--no-extra-strip` are the only sensible candidates to consider setting
|
350
|
+
as defaults.
|
347
351
|
|
348
352
|
## Command Line Tab Completion
|
349
353
|
|
@@ -376,5 +380,6 @@ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License at
|
|
376
380
|
[pdmpy]: https://pdm-project.org/en/latest/usage/project/#install-python-interpreters-with-pdm
|
377
381
|
[hatch]: https://hatch.pypa.io/
|
378
382
|
[hatchpy]: https://hatch.pypa.io/latest/tutorials/python/manage/
|
383
|
+
[ryepy]: https://rye.astral.sh/guide/toolchains/#fetching-toolchains
|
379
384
|
|
380
385
|
<!-- vim: se ai syn=markdown: -->
|
@@ -0,0 +1,6 @@
|
|
1
|
+
pystand.py,sha256=UoQBHL_ORiI56c3Z3j2Sw6FubW4h8kAxx5dZ6ZIfty0,30499
|
2
|
+
pystand-1.7.dist-info/METADATA,sha256=NhfQfiLnwzegtPlSnORtFAG1L3usbwFAWTBO3-hbZsA,15105
|
3
|
+
pystand-1.7.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
4
|
+
pystand-1.7.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
+
pystand-1.7.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
+
pystand-1.7.dist-info/RECORD,,
|
pystand.py
CHANGED
@@ -40,6 +40,8 @@ FIRST_RELEASE = '20210724'
|
|
40
40
|
# Sample release tag for documentation/usage examples
|
41
41
|
SAMPL_RELEASE = '20240415'
|
42
42
|
|
43
|
+
STRIP_EXT = '_stripped'
|
44
|
+
|
43
45
|
# The following regexp pattern is used to match the end of a release file name
|
44
46
|
ENDPAT = r'-install_only[-\dT]*.tar.gz$'
|
45
47
|
|
@@ -234,14 +236,22 @@ def get_release_tag(args: Namespace) -> str:
|
|
234
236
|
args._latest_release.write_text(tag + '\n')
|
235
237
|
return tag
|
236
238
|
|
237
|
-
def
|
238
|
-
'
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
#
|
243
|
-
#
|
244
|
-
|
239
|
+
def merge(files: dict, name: str, url: str, stripped: bool) -> None:
|
240
|
+
'Merge a file into the files dict'
|
241
|
+
# We store: A string url when there is no stripped version; or a
|
242
|
+
# tuple of string urls, the first unstripped and the second
|
243
|
+
# stripped. It's a bit messy but we are keeping compatibility with
|
244
|
+
# earlier releases (and their cached release files) which did not
|
245
|
+
# have stripped versions.
|
246
|
+
impl, ver, distrib = name.split('-', maxsplit=2)
|
247
|
+
if impl not in files:
|
248
|
+
files[impl] = defaultdict(dict)
|
249
|
+
|
250
|
+
if exist := files[impl].get(ver, {}).get(distrib, {}):
|
251
|
+
files[impl][ver][distrib] = (exist, url) \
|
252
|
+
if stripped else (url, exist[1])
|
253
|
+
else:
|
254
|
+
files[impl][ver][distrib] = ('', url) if stripped else url
|
245
255
|
|
246
256
|
def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
|
247
257
|
'Return the release files for the given tag'
|
@@ -260,17 +270,19 @@ def get_release_files(args, tag, implementation: Optional[str] = None) -> dict:
|
|
260
270
|
except Exception:
|
261
271
|
return {}
|
262
272
|
|
263
|
-
# Iterate over the release assets and store
|
264
|
-
# return
|
265
|
-
for
|
266
|
-
|
267
|
-
|
273
|
+
# Iterate over the release assets and store pertinent files in a
|
274
|
+
# dict to return.
|
275
|
+
for asset in release.get_assets():
|
276
|
+
name = asset.name
|
277
|
+
if is_stripped := (STRIP_EXT in name):
|
278
|
+
name = name.replace(STRIP_EXT, '')
|
268
279
|
|
269
|
-
|
270
|
-
|
271
|
-
files
|
280
|
+
if m := re.search(ENDPAT, name):
|
281
|
+
name = re.sub(r'\+\d{8}', '', name[:m.start()])
|
282
|
+
merge(files, name, asset.browser_download_url, is_stripped)
|
272
283
|
|
273
|
-
|
284
|
+
if not files:
|
285
|
+
sys.exit(f'Failed to fetch any files for release {tag}')
|
274
286
|
|
275
287
|
if error := set_json(jfile, files):
|
276
288
|
sys.exit(f'Failed to write release {tag} file {jfile}: {error}')
|
@@ -309,13 +321,13 @@ def update_version_symlinks(args: Namespace) -> None:
|
|
309
321
|
for name, tgt in oldlinks.items():
|
310
322
|
new_tgt = newlinks.get(name)
|
311
323
|
if not new_tgt or new_tgt != tgt:
|
312
|
-
|
324
|
+
(base / name).unlink()
|
313
325
|
|
314
326
|
# Create all needed new links
|
315
327
|
for name, tgt in newlinks.items():
|
316
328
|
old_tgt = oldlinks.get(name)
|
317
329
|
if not old_tgt or old_tgt != tgt:
|
318
|
-
|
330
|
+
(base / name).symlink_to(tgt, target_is_directory=True)
|
319
331
|
|
320
332
|
def purge_unused_releases(args: Namespace) -> None:
|
321
333
|
'Purge old releases that are no longer needed and have expired'
|
@@ -368,34 +380,43 @@ def remove(args: Namespace, version: str) -> None:
|
|
368
380
|
|
369
381
|
shutil.rmtree(vdir)
|
370
382
|
|
371
|
-
def
|
383
|
+
def strip_binaries(vdir: Path, distribution: str) -> bool:
|
372
384
|
'Strip binaries from files in a version directory'
|
373
385
|
# Only run the strip command on Linux hosts and for Linux distributions
|
374
|
-
|
375
|
-
|
386
|
+
was_stripped = False
|
387
|
+
if platform.system() == 'Linux' and '-linux-' in distribution:
|
388
|
+
for path in ('bin', 'lib'):
|
389
|
+
base = vdir / path
|
390
|
+
if not base.is_dir():
|
391
|
+
continue
|
376
392
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
393
|
+
for file in base.iterdir():
|
394
|
+
if not file.is_symlink() and file.is_file():
|
395
|
+
cmd = f'strip -p --strip-unneeded {file}'.split()
|
396
|
+
try:
|
397
|
+
subprocess.run(cmd, stderr=subprocess.DEVNULL)
|
398
|
+
except Exception:
|
399
|
+
pass
|
400
|
+
else:
|
401
|
+
was_stripped = True
|
381
402
|
|
382
|
-
|
383
|
-
if not file.is_symlink() and file.is_file():
|
384
|
-
cmd = f'strip -p --strip-unneeded {file}'.split()
|
385
|
-
try:
|
386
|
-
subprocess.run(cmd, stderr=subprocess.DEVNULL)
|
387
|
-
except Exception:
|
388
|
-
pass
|
403
|
+
return was_stripped
|
389
404
|
|
390
405
|
def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
391
406
|
files: dict) -> Optional[str]:
|
392
407
|
'Install a version'
|
393
408
|
version = vdir.name
|
394
409
|
|
395
|
-
if not (
|
410
|
+
if not (fileurl := files[version].get(distribution)):
|
396
411
|
return f'Arch "{distribution}" not found for release '\
|
397
412
|
f'{release} version {version}.'
|
398
413
|
|
414
|
+
if isinstance(fileurl, tuple):
|
415
|
+
stripped = not args.no_strip
|
416
|
+
fileurl = fileurl[stripped]
|
417
|
+
else:
|
418
|
+
stripped = False
|
419
|
+
|
399
420
|
tmpdir = args._versions / f'.{version}-tmp'
|
400
421
|
rm_path(tmpdir)
|
401
422
|
tmpdir.mkdir()
|
@@ -403,19 +424,32 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
403
424
|
error = None
|
404
425
|
|
405
426
|
try:
|
406
|
-
urllib.request.urlretrieve(
|
427
|
+
urllib.request.urlretrieve(fileurl, tmpdir / 'tmp.tar.gz')
|
428
|
+
except Exception as e:
|
429
|
+
error = f'Failed to fetch "{fileurl}": {e}'
|
430
|
+
|
431
|
+
try:
|
407
432
|
shutil.unpack_archive(tmpdir / 'tmp.tar.gz', tmpdir)
|
408
433
|
except Exception as e:
|
409
|
-
error = f'Failed to
|
434
|
+
error = f'Failed to unpack "{fileurl}": {e}'
|
410
435
|
|
411
436
|
if not error:
|
412
437
|
data = {'release': release, 'distribution': distribution}
|
438
|
+
|
439
|
+
if args.no_strip:
|
440
|
+
data['stripped'] = 'forced off'
|
441
|
+
elif stripped and args.no_extra_strip:
|
442
|
+
data['stripped'] = 'at_source_only'
|
443
|
+
else:
|
444
|
+
if strip_binaries(tmpdir_py, distribution):
|
445
|
+
data['stripped'] = 'at_source_and_manually' \
|
446
|
+
if stripped else 'manually'
|
447
|
+
else:
|
448
|
+
data['stripped'] = 'at_source' if stripped else 'n/a'
|
449
|
+
|
413
450
|
if (error := set_json(tmpdir_py / args._data, data)):
|
414
451
|
error = f'Failed to write {version} data file: {error}'
|
415
452
|
|
416
|
-
if not args.do_not_strip:
|
417
|
-
strip(tmpdir_py, distribution)
|
418
|
-
|
419
453
|
if not error:
|
420
454
|
remove(args, version)
|
421
455
|
tmpdir_py.replace(vdir)
|
@@ -426,7 +460,7 @@ def install(args: Namespace, vdir: Path, release: str, distribution: str,
|
|
426
460
|
def main() -> Optional[str]:
|
427
461
|
'Main code'
|
428
462
|
distro_default = DISTRIBUTIONS.get((platform.system(), platform.machine()))
|
429
|
-
distro_help = distro_default
|
463
|
+
distro_help = distro_default or '?unknown?'
|
430
464
|
|
431
465
|
base_dir = Path('/opt' if is_admin() else
|
432
466
|
platformdirs.user_data_dir()) / PROG
|
@@ -456,8 +490,10 @@ def main() -> Optional[str]:
|
|
456
490
|
opt.add_argument('--github-access-token',
|
457
491
|
help='optional Github access token. Can specify to reduce '
|
458
492
|
'rate limiting.')
|
459
|
-
opt.add_argument('--
|
460
|
-
help='
|
493
|
+
opt.add_argument('--no-strip', action='store_true',
|
494
|
+
help='do not use or create stripped binaries')
|
495
|
+
opt.add_argument('--no-extra-strip', action='store_true',
|
496
|
+
help='do not restrip already stripped source binaries')
|
461
497
|
opt.add_argument('-V', action='store_true',
|
462
498
|
help=f'show {PROG} version')
|
463
499
|
cmd = opt.add_subparsers(title='Commands', dest='cmdname')
|
pystand-1.5.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
pystand.py,sha256=L0SAV5snhVrNrhFWTb4xuLHp3Mr5eNYF6ke_AWdxtq0,29019
|
2
|
-
pystand-1.5.dist-info/METADATA,sha256=j9q9s0eDv4reAYspvONxUrFvduSaeef8tFx9IzEfVaQ,14843
|
3
|
-
pystand-1.5.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
|
4
|
-
pystand-1.5.dist-info/entry_points.txt,sha256=DG4ps3I3nni1bubV1tXs6u8FARgkdbAYaEAzZD4RAo8,41
|
5
|
-
pystand-1.5.dist-info/top_level.txt,sha256=NoWUh19UQymAJLHTCdxMnVwV6Teftef5fzyF3OWLyNY,8
|
6
|
-
pystand-1.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|