ezpkg 0.0.1__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.
- ezpkg-0.0.1.dist-info/METADATA +595 -0
- ezpkg-0.0.1.dist-info/RECORD +4 -0
- ezpkg-0.0.1.dist-info/WHEEL +4 -0
- ezpkg.py +9098 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ezpkg
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Automatic Package Fetching for One-File Programs
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
License: MIT No Attribution
|
|
7
|
+
Author: Wayne C. Gramlich
|
|
8
|
+
Project-URL: Homepage, https://github.com/waynegramlich/ezpkg
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# `EZPkg`: Easy Python Packages
|
|
17
|
+
|
|
18
|
+
`EZPkg` (pronounced "easy package") grew out of the desire to send a simple
|
|
19
|
+
one-file Python program to a friend. When said friend tried to run it,
|
|
20
|
+
a few packages were missing, so the friend had to create a virtual environment
|
|
21
|
+
and install them. It worked, but it was annoying. So the question became
|
|
22
|
+
"Could this be made really easy for the recipient?" Thus was born `EZPkg`,
|
|
23
|
+
which lets a single-file Python program download its missing packages all
|
|
24
|
+
by itself.
|
|
25
|
+
|
|
26
|
+
To get started with `EZPkg`, you paste a modest bootstrap at the front of
|
|
27
|
+
your program and go. After that, `EZPkg` fetches your packages over the net
|
|
28
|
+
on demand. Once everything is working with `EZPkg`, `EZPkg` can easily make
|
|
29
|
+
a release in one of three formats - "email", "zipapp", or "uv".
|
|
30
|
+
|
|
31
|
+
Table of Contents:
|
|
32
|
+
* [Quick Start](#quick-start)
|
|
33
|
+
* [Running a Program That Uses `EZPkg`](#running-a-program-that-uses-ezpkg)
|
|
34
|
+
* [Enabling `EZPkg` for a Program](#enabling-ezpkg-for-a-program)
|
|
35
|
+
* [Releasing a Packaged Program](#releasing-a-packaged-program)
|
|
36
|
+
* [The Bootstrap](#the-bootstrap)
|
|
37
|
+
* [Security](#security)
|
|
38
|
+
* [Miscellaneous](#miscellaneous)
|
|
39
|
+
* [How `EZPkg` Actually Works](#how-ezpkg-actually-works)
|
|
40
|
+
* [Troubleshooting](#troubleshooting)
|
|
41
|
+
* [Project Information](#project-information)
|
|
42
|
+
* [Conclusion](#conclusion)
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
To get started, please take the program below and paste it into a file
|
|
47
|
+
named `quick_start.py` on your computer. The bulk of this program is the
|
|
48
|
+
`EZPkg` bootstrap, Part 2. This is the "magic" that skips the
|
|
49
|
+
hassle of virtual environments. Then run it as the next section shows.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
#!/usr/bin/env python3
|
|
53
|
+
# Part 1: These lines look like `#` comments, but they are Python's inline
|
|
54
|
+
# script metadata (PEP 723) that the EZPkg bootstrap needs. Never remove them.
|
|
55
|
+
# /// script
|
|
56
|
+
# requires-python = ">=3.11"
|
|
57
|
+
# dependencies = ["ezpkg>=0.0.1.dev0,<0.1", "cowsay"]
|
|
58
|
+
# ///
|
|
59
|
+
"""Try `EZPkg`: fetch the `cowsay` package on first use, then run."""
|
|
60
|
+
|
|
61
|
+
# Part 2: The EZPkg bootstrap. Paste all of it, `# Begin` to `# End`,
|
|
62
|
+
# below your docstring and any `from __future__` lines.
|
|
63
|
+
# Begin the EZPkg bootstrap. Never edit it.
|
|
64
|
+
import hashlib
|
|
65
|
+
import importlib
|
|
66
|
+
import json
|
|
67
|
+
import pathlib
|
|
68
|
+
import subprocess
|
|
69
|
+
import sys
|
|
70
|
+
import urllib.request as req
|
|
71
|
+
|
|
72
|
+
try: # Do not edit try block! See https://pypi.org/project/ezpkg/
|
|
73
|
+
s = "find ezpkg"; t = pathlib.Path.home()/".ezpkg"; w = t/"ezpkg.whl"
|
|
74
|
+
def g(u): # Fetch u, with curl if this Python has no certificates.
|
|
75
|
+
try: return req.urlopen(u, None, 60).read()
|
|
76
|
+
except req.URLError as y: z = y # As on python.org's macOS Python.
|
|
77
|
+
try: return subprocess.run(["curl", "-fsSL", u],
|
|
78
|
+
capture_output=True, check=True).stdout
|
|
79
|
+
except Exception: raise z from None
|
|
80
|
+
try: m = None; m = importlib.import_module("ezpkg") # Already importable?
|
|
81
|
+
except ImportError: pass
|
|
82
|
+
if m is None and not w.is_file(): # Not downloaded yet.
|
|
83
|
+
s = "download ezpkg"; d = g("https://pypi.org/pypi/ezpkg/json")
|
|
84
|
+
e = next(e for e in json.loads(d)["urls"] if e["url"][-4:] == ".whl")
|
|
85
|
+
r = g(e["url"])
|
|
86
|
+
if hashlib.sha256(r).hexdigest() != e["digests"]["sha256"]:
|
|
87
|
+
raise ValueError(f"{e['filename']} fails its SHA-256 check")
|
|
88
|
+
t.mkdir(parents=True, exist_ok=True); p = t/"ezpkg.part"
|
|
89
|
+
p.write_bytes(r); p.replace(w) # The rename is atomic.
|
|
90
|
+
if m is None: # Python imports it straight from the wheel.
|
|
91
|
+
s = "import ezpkg"; sys.path.insert(0, str(w))
|
|
92
|
+
m = importlib.import_module("ezpkg")
|
|
93
|
+
s = "fetch packages"; m.EZPkg().install(bootstrap="2026-10-23")
|
|
94
|
+
except Exception as x:
|
|
95
|
+
raise SystemExit(f"ezpkg: {s} failed: {x!r}") # Do not edit try block!
|
|
96
|
+
# End the EZPkg bootstrap. Your imports go below it.
|
|
97
|
+
|
|
98
|
+
# Part 3: Your imports, all after the bootstrap.
|
|
99
|
+
import cowsay # type: ignore[import-not-found] # For mypy.
|
|
100
|
+
|
|
101
|
+
# Part 4: The program itself.
|
|
102
|
+
cowsay.cow("EZPkg works!")
|
|
103
|
+
print(f"The cow above was drawn by cowsay {cowsay.__version__}.")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Running a Program That Uses `EZPkg`
|
|
107
|
+
|
|
108
|
+
The `quick_start.py` program is run as follows:
|
|
109
|
+
* Windows: `py quick_start.py` or `python quick_start.py`
|
|
110
|
+
* Linux/macOS: `python3 quick_start.py`
|
|
111
|
+
|
|
112
|
+
The first time `quick_start.py` runs, it prints the following:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
The author of `quick_start.py` has selected some software packages from the
|
|
116
|
+
internet. If you agree, they are retained in `~/.ezpkg`. If you do not trust
|
|
117
|
+
the author, answer `n` and `~/.ezpkg` will simply go away without running
|
|
118
|
+
`quick_start.py`.
|
|
119
|
+
|
|
120
|
+
Retain `~/.ezpkg` and run `quick_start.py`?
|
|
121
|
+
[y=>Yes|n=>No|?=>Explain]:
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The three options are:
|
|
125
|
+
* `y=>Yes`:
|
|
126
|
+
Answer `y`, and `EZPkg` lists what it retained in `~/.ezpkg` (only this
|
|
127
|
+
once), and then the program runs.
|
|
128
|
+
* `n=>No`:
|
|
129
|
+
The newly downloaded packages are removed and the program does not run.
|
|
130
|
+
On the very first run, all of `~/.ezpkg` is removed. Pressing Control-C
|
|
131
|
+
or not answering within 5 minutes is treated as `n`.
|
|
132
|
+
* `?=>Explain`:
|
|
133
|
+
This lists the packages in question and repeats the question.
|
|
134
|
+
|
|
135
|
+
Lastly, the prompt is skipped when there is nobody at a terminal to answer it
|
|
136
|
+
(e.g. `cron` or a test system), and `y` is assumed.
|
|
137
|
+
|
|
138
|
+
After a `y`, the first run generally looks as follows, though the version
|
|
139
|
+
numbers may differ:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
The following 3 package(s) are now retained in `~/.ezpkg`:
|
|
143
|
+
1. pip 26.2.1 (used by `ezpkg`): The PyPA recommended tool for installing Py...
|
|
144
|
+
2. ezpkg 0.0.1: Automatic Package Fetching for One-File Programs
|
|
145
|
+
3. cowsay 6.1: The famous cowsay for GNU/Linux is now available for python
|
|
146
|
+
`ezpkg` (https://pypi.org/project/ezpkg/) manages `~/.ezpkg`.
|
|
147
|
+
|
|
148
|
+
____________
|
|
149
|
+
| EZPkg works! |
|
|
150
|
+
============
|
|
151
|
+
\
|
|
152
|
+
\
|
|
153
|
+
^__^
|
|
154
|
+
(oo)\_______
|
|
155
|
+
(__)\ )\/\
|
|
156
|
+
||----w |
|
|
157
|
+
|| ||
|
|
158
|
+
The cow above was drawn by cowsay 6.1.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The list includes `pip` and `ezpkg` only on a machine's very first run,
|
|
162
|
+
when they have just been fetched; `pip` is the tool `ezpkg` uses,
|
|
163
|
+
not one the program's author chose. On Windows, the full path, such as
|
|
164
|
+
`C:\Users\YOUR_NAME\.ezpkg`, is shown instead of `~/.ezpkg`.
|
|
165
|
+
|
|
166
|
+
Now run it a second time. There is no question and no download, and it
|
|
167
|
+
starts in a fraction of a second, because `cowsay` is already in
|
|
168
|
+
`~/.ezpkg/`. The next time you run the program, you will not be asked for
|
|
169
|
+
permission to store files in `~/.ezpkg/`.
|
|
170
|
+
|
|
171
|
+
There are basically 4 things that can go wrong:
|
|
172
|
+
* **No Network**:
|
|
173
|
+
The network is needed only when a program needs packages that are not
|
|
174
|
+
yet in `~/.ezpkg`.
|
|
175
|
+
* **No Version Available**:
|
|
176
|
+
The version the author chose is no longer on PyPI, or none was built
|
|
177
|
+
for your computer and Python.
|
|
178
|
+
* **`pip` Failure**:
|
|
179
|
+
`pip` ("`pip` Installs Packages") is the standard tool that fetches
|
|
180
|
+
Python packages, and it can fail for other reasons.
|
|
181
|
+
* **Not Approved**:
|
|
182
|
+
The author sent a `.py` that has not been produced as an `EZPkg` release,
|
|
183
|
+
and this computer needs a package the author's computer did not. The
|
|
184
|
+
solution is to ask the author for a released version of the program; see
|
|
185
|
+
[Releasing a Packaged Program](#releasing-a-packaged-program).
|
|
186
|
+
|
|
187
|
+
For each issue above, one line is printed and the program does not run.
|
|
188
|
+
For further details of what went wrong, read `~/.ezpkg/last_failure.txt`,
|
|
189
|
+
which every failure rewrites.
|
|
190
|
+
|
|
191
|
+
If you want to remove the `~/.ezpkg` directory and its contents, do the
|
|
192
|
+
following:
|
|
193
|
+
* Windows (PowerShell): `py $HOME\.ezpkg\remove.py`
|
|
194
|
+
* Windows (`cmd.exe`): `py %USERPROFILE%\.ezpkg\remove.py`
|
|
195
|
+
* Linux/macOS: `python3 ~/.ezpkg/remove.py`
|
|
196
|
+
|
|
197
|
+
While it is always safe to remove `~/.ezpkg`, please note that if you run
|
|
198
|
+
`quick_start.py` again, the `~/.ezpkg` directory will return.
|
|
199
|
+
|
|
200
|
+
## Enabling `EZPkg` for a Program
|
|
201
|
+
|
|
202
|
+
To enable `EZPkg` in your program `PROG_NAME.py` (with `PROG_NAME` replaced
|
|
203
|
+
by your program's name):
|
|
204
|
+
1. Copy Part 2 of `quick_start.py`, from `# Begin the EZPkg bootstrap.`
|
|
205
|
+
through `# End the EZPkg bootstrap.`, and paste it below your program's
|
|
206
|
+
top-level docstring (and any `from __future__` lines), but above your own
|
|
207
|
+
imports.
|
|
208
|
+
2. Beside `PROG_NAME.py`, create an empty `PROG_NAME.py.toml`:
|
|
209
|
+
* Windows (PowerShell): `ni PROG_NAME.py.toml`
|
|
210
|
+
* Windows (`cmd.exe`): `type nul > PROG_NAME.py.toml`
|
|
211
|
+
* Linux/macOS: `touch PROG_NAME.py.toml`
|
|
212
|
+
3. Run `PROG_NAME.py`.
|
|
213
|
+
|
|
214
|
+
The `PROG_NAME.py.toml` makes you the program's developer: `EZPkg` asks you
|
|
215
|
+
nothing and fetches whatever your imports need. Your `PROG_NAME.py` is never
|
|
216
|
+
edited; a release writes the packages and their versions into the copies
|
|
217
|
+
it makes. On the first run, `EZPkg` fills the empty `PROG_NAME.py.toml` with
|
|
218
|
+
its settings, each commented out at its default; see
|
|
219
|
+
[`PROG_NAME.py.toml` Settings](#progpytoml-settings). Without
|
|
220
|
+
`PROG_NAME.py.toml`, whoever runs `PROG_NAME.py` is an end user, the same
|
|
221
|
+
as in the Quick Start.
|
|
222
|
+
|
|
223
|
+
### What are packages and where do they come from?
|
|
224
|
+
|
|
225
|
+
Since the primary purpose of `EZPkg` is to manage Python packages, it is
|
|
226
|
+
prudent to discuss a little bit more about them.
|
|
227
|
+
|
|
228
|
+
In short, a package is a chunk of code that can be imported into a Python
|
|
229
|
+
program for subsequent execution. The most common place to fetch packages
|
|
230
|
+
from is the [Python Package Index](https://pypi.org) (PyPI), which is where
|
|
231
|
+
the Python community submits freely accessible packages.
|
|
232
|
+
|
|
233
|
+
A package evolves over time, undergoing both bug fixes and feature
|
|
234
|
+
enhancements. These changes are managed by giving each released package a
|
|
235
|
+
different version number. Furthermore, for performance reasons, some packages
|
|
236
|
+
are compiled to run on the native machine architecture (e.g. Intel, ARM,
|
|
237
|
+
RISC-V). Likewise, some packages require specific features available only on a
|
|
238
|
+
specific operating system (e.g. Windows, Linux (Red Hat, Ubuntu), macOS).
|
|
239
|
+
|
|
240
|
+
Finding a set of packages that can interoperate with one another can be quite
|
|
241
|
+
challenging. Ultimately, `EZPkg` encodes the package configuration in Python
|
|
242
|
+
inline script metadata format at the beginning of each released copy of the
|
|
243
|
+
Python file. It is unlikely that you will ever need to read any of this
|
|
244
|
+
metadata. If you look at the `quick_start.py` example, the information between
|
|
245
|
+
the `# /// script` and `# ///` lines is a very short example of the metadata.
|
|
246
|
+
`EZPkg` accomplishes all of this by scanning your Python program
|
|
247
|
+
for all `import` statements and reconciling them into a single, coherent,
|
|
248
|
+
versioned package set.
|
|
249
|
+
|
|
250
|
+
Frankly, `EZPkg` is trying to do all of the hard work of managing the
|
|
251
|
+
metadata and other stuff so that you do not need to. However, if you are curious,
|
|
252
|
+
the following references provide a great deal of additional information:
|
|
253
|
+
* [Python Package Index](https://pypi.org/):
|
|
254
|
+
The most common place to store freely accessible Python packages.
|
|
255
|
+
* [Inline script metadata](
|
|
256
|
+
https://packaging.python.org/en/latest/specifications/inline-script-metadata):
|
|
257
|
+
The Python standard inline configuration information.
|
|
258
|
+
* [TOML (Tom's Obvious Minimal Language)](https://toml.io/en/) format:
|
|
259
|
+
This is the syntax used for the inline script metadata.
|
|
260
|
+
* [Dependency specifiers](
|
|
261
|
+
https://packaging.python.org/en/latest/specifications/dependency-specifiers):
|
|
262
|
+
This is the syntax of each entry in `dependencies=[...]`.
|
|
263
|
+
|
|
264
|
+
### `PROG_NAME.py.toml` Settings
|
|
265
|
+
|
|
266
|
+
When PROG_NAME.py runs and finds an empty PROG_NAME.py.toml file, EZPkg
|
|
267
|
+
fills it with exactly the content shown below (i.e. from
|
|
268
|
+
"# Start of PROG_NAME.py.toml:" to "# End of PROG_NAME.py.toml".)
|
|
269
|
+
|
|
270
|
+
```toml
|
|
271
|
+
# Start of PROG_NAME.py.toml:
|
|
272
|
+
|
|
273
|
+
# This file is meant to be self-documenting. It contains the EZPkg settings
|
|
274
|
+
# for PROG_NAME.py (i.e. your program). More: https://pypi.org/project/ezpkg/
|
|
275
|
+
|
|
276
|
+
# This file is created as an empty file next to PROG_NAME.py, in the same
|
|
277
|
+
# directory. Depending upon which operating system you are running, one of
|
|
278
|
+
# the following commands creates the empty file:
|
|
279
|
+
# * Windows (PowerShell): `ni PROG_NAME.py.toml`
|
|
280
|
+
# * Windows (`cmd.exe`): `type nul > PROG_NAME.py.toml`
|
|
281
|
+
# * Linux/macOS: `touch PROG_NAME.py.toml`
|
|
282
|
+
|
|
283
|
+
# The mere existence of PROG_NAME.py.toml classifies you as a developer of
|
|
284
|
+
# PROG_NAME.py. Conversely, if you remove this file, you are no longer
|
|
285
|
+
# treated as a developer. Instead, you will be asked on the program's first
|
|
286
|
+
# run whether its packages can be kept in the ~/.ezpkg directory.
|
|
287
|
+
|
|
288
|
+
# Whenever PROG_NAME.py.toml is empty and PROG_NAME.py is run, its contents
|
|
289
|
+
# are initialized to the text you are currently reading (i.e. from
|
|
290
|
+
# "# Start of PROG_NAME.py.toml:" to "# End of PROG_NAME.py.toml".) After
|
|
291
|
+
# this file is created, only you (the developer) get to edit it. If you
|
|
292
|
+
# want it reinitialized, empty the file and run PROG_NAME.py again.
|
|
293
|
+
|
|
294
|
+
# This file is the only place where your EZPkg rules are configured. Any
|
|
295
|
+
# additional information collected by EZPkg is stored in various files in
|
|
296
|
+
# the `~/.ezpkg/` directory. EZPkg only ever fills this file when it is
|
|
297
|
+
# empty, and never modifies it after that.
|
|
298
|
+
|
|
299
|
+
# All possible settings that can be set are shown as commented-out TOML
|
|
300
|
+
# lines. To enable an option, just remove the preceding "# ". For a list,
|
|
301
|
+
# such as "release", remove the "#" from each entry you want. If you
|
|
302
|
+
# accidentally enable two conflicting options, EZPkg will produce an error
|
|
303
|
+
# message with line numbers for you to fix.
|
|
304
|
+
|
|
305
|
+
# The following configuration options exist:
|
|
306
|
+
|
|
307
|
+
# "verbosity" specifies how much EZPkg prints while the program runs.
|
|
308
|
+
# There are 4 levels of verbosity. The default is "low":
|
|
309
|
+
# verbosity = "high" # Also what EZPkg decided, and why.
|
|
310
|
+
# verbosity = "medium" # Also each change to the packages.
|
|
311
|
+
# verbosity = "low" # Only one-off notices (default).
|
|
312
|
+
# verbosity = "none" # Nothing, except errors that stop the program.
|
|
313
|
+
|
|
314
|
+
# "update-frequency" specifies how often your runs move to newer package
|
|
315
|
+
# versions. A release always carries the versions your last run used, so
|
|
316
|
+
# whoever receives it gets exactly what you tested.
|
|
317
|
+
# update-frequency = "never" # Leave the current versions alone (default).
|
|
318
|
+
# update-frequency = "monthly" # Move to the newest versions once a month.
|
|
319
|
+
# update-frequency = "weekly" # Move to the newest versions once a week.
|
|
320
|
+
# update-frequency = "daily" # Move to the newest versions once a day.
|
|
321
|
+
|
|
322
|
+
# "purge-unused-after" specifies how long a package may go unused before
|
|
323
|
+
# EZPkg removes it from ~/.ezpkg (it is fetched again if it is needed).
|
|
324
|
+
# purge-unused-after = "month" # Unused for a month (default).
|
|
325
|
+
# purge-unused-after = "week" # Unused for a week.
|
|
326
|
+
# purge-unused-after = "day" # Unused for a day.
|
|
327
|
+
|
|
328
|
+
# "version" specifies the release string. Whenever it differs from the
|
|
329
|
+
# last version released, the next run builds the releases listed in
|
|
330
|
+
# "release", and then runs the program as usual. Change it whenever you
|
|
331
|
+
# like. If you really need to, you can even reuse a previous value.
|
|
332
|
+
# version = "1.0.0" # 1.0.0 is usually the first functional release number.
|
|
333
|
+
|
|
334
|
+
# "release" lists which releases to generate, each named with the VERSION
|
|
335
|
+
# above. Remove the "#" from 0, 1, or more of the entries in the list.
|
|
336
|
+
release = [
|
|
337
|
+
# "email", # PROG_NAME-VERSION.py; email .py (network needed).
|
|
338
|
+
# "zipapp", # PROG_NAME-VERSION.pyz; pure Python (no network needed).
|
|
339
|
+
# "uv", # PROG_NAME-VERSION-uv.py and its lock (`uv` needed).
|
|
340
|
+
]
|
|
341
|
+
# Each release, and anything worth a second look, is recorded in
|
|
342
|
+
# PROG_NAME.py.releases.
|
|
343
|
+
|
|
344
|
+
# "bootstrap" specifies which EZPkg bootstrap the program carries. Each
|
|
345
|
+
# bootstrap is named by the date it was published ("YYYY-MM-DD").
|
|
346
|
+
# bootstrap = "update" # A release replaces an older one (default).
|
|
347
|
+
# bootstrap = "2026-10-23" # Keep exactly this one.
|
|
348
|
+
|
|
349
|
+
# The next options are rarely needed. Most people can stop reading now.
|
|
350
|
+
# They are needed for difficult situations that EZPkg simply cannot figure
|
|
351
|
+
# out on its own.
|
|
352
|
+
|
|
353
|
+
# "requires-python" specifies the oldest Python the program runs on. EZPkg
|
|
354
|
+
# works this out from the program and its packages (never older than 3.11,
|
|
355
|
+
# which EZPkg itself needs). Set it only when the program needs a newer
|
|
356
|
+
# Python for its own reasons, such as a library function that is new.
|
|
357
|
+
# requires-python = ">=3.11" # The default: EZPkg itself needs 3.11.
|
|
358
|
+
|
|
359
|
+
# "dependencies" provides a way to manually specify values to be inserted
|
|
360
|
+
# into the dependencies portion of the Python inline script metadata. They
|
|
361
|
+
# use the standard Python dependency specifier format. See:
|
|
362
|
+
#
|
|
363
|
+
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/
|
|
364
|
+
#
|
|
365
|
+
# Put every entry in the one list, which may span several lines. If an
|
|
366
|
+
# entry conflicts with what EZPkg would choose, your entry wins.
|
|
367
|
+
# dependencies = ["numpy>=2.1", "requests[socks]"] # For example.
|
|
368
|
+
|
|
369
|
+
# End of PROG_NAME.py.toml.
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Releasing a Packaged Program
|
|
373
|
+
|
|
374
|
+
Once you are satisfied with your program, you may choose to release it in
|
|
375
|
+
a more durable single file package. Currently three formats are supported:
|
|
376
|
+
* **`email`**:
|
|
377
|
+
Email format is just a single Python file, `PROG_NAME-VERSION.py`: a copy
|
|
378
|
+
of `PROG_NAME.py` with its inline metadata filled in. When emailed to
|
|
379
|
+
another person, the `EZPkg` bootstrap ensures that all of the required
|
|
380
|
+
packages are downloaded prior to running the program.
|
|
381
|
+
* **`zipapp`**:
|
|
382
|
+
This works for pure Python programs (i.e. no packages with C, C++, Rust,
|
|
383
|
+
etc.) and produces a `.pyz` file. The file can be quite large since it
|
|
384
|
+
includes all of the needed code. All that the recipient needs is a Python
|
|
385
|
+
interpreter; no network is needed. It produces a file
|
|
386
|
+
`PROG_NAME-X.Y.Z.pyz`, where `PROG_NAME` is the program name and `X.Y.Z`
|
|
387
|
+
is the version number.
|
|
388
|
+
* **`uv`**:
|
|
389
|
+
`uv` is a popular packaging system written in Rust. It handles both
|
|
390
|
+
compiled and pure Python packages together. The recipient must have `uv`
|
|
391
|
+
installed, and `uv` fetches the packages over the network on the first run.
|
|
392
|
+
|
|
393
|
+
The requirement is that at least one of the values for `release = [ ... ]`
|
|
394
|
+
is enabled (`email`, `zipapp` or `uv`), and that `version` is set. Once
|
|
395
|
+
both are set, run your program. As a side effect of running your program
|
|
396
|
+
the appropriate files are generated. Whenever `version` changes, the
|
|
397
|
+
appropriate files are (re)generated.
|
|
398
|
+
|
|
399
|
+
## The Bootstrap
|
|
400
|
+
|
|
401
|
+
Never edit the bootstrap, not even a space or a comment. Indenting the
|
|
402
|
+
whole of it, as under an `if`, is the one change allowed. Anything else
|
|
403
|
+
stops the program, which shows each line that differs from the published
|
|
404
|
+
bootstrap.
|
|
405
|
+
|
|
406
|
+
An older bootstrap that has not been edited still runs, so a program that
|
|
407
|
+
nobody maintains will keep working. When you are the developer, `EZPkg` tells
|
|
408
|
+
you once about each newer bootstrap, and the next release replaces it.
|
|
409
|
+
|
|
410
|
+
## Security
|
|
411
|
+
|
|
412
|
+
`EZPkg` fetches code from the internet and runs it, so here is who you
|
|
413
|
+
are trusting:
|
|
414
|
+
* **The program's author**:
|
|
415
|
+
The one question on a first run is about them. If you do not trust
|
|
416
|
+
them, answer `n`. A `y` is remembered, so you are not asked again.
|
|
417
|
+
* **PyPI**:
|
|
418
|
+
Every package, `ezpkg` included, comes from the
|
|
419
|
+
[Python Package Index](https://pypi.org), and each download is checked
|
|
420
|
+
against the checksum PyPI publishes for it.
|
|
421
|
+
* **No substitutions allowed**:
|
|
422
|
+
Once its author has run a program with `EZPkg`, the program carries a
|
|
423
|
+
checksum of every package file it ran with. A file that differs, such
|
|
424
|
+
as one re-uploaded to PyPI later, is refused before anything in it
|
|
425
|
+
can run.
|
|
426
|
+
* **All content is stored in the `~/.ezpkg` directory**, except:
|
|
427
|
+
* `PROG_NAME.py.toml`, which is filled only when it is empty.
|
|
428
|
+
* Up to 4 output files in total for the three possible release
|
|
429
|
+
formats: "email" (1), "zipapp" (1) and "uv" (2).
|
|
430
|
+
|
|
431
|
+
## Miscellaneous
|
|
432
|
+
|
|
433
|
+
Here are some miscellaneous observations:
|
|
434
|
+
* **Garbage collection**:
|
|
435
|
+
Packages that no program has used for 30 days are removed, unless a
|
|
436
|
+
program's `PROG_NAME.py.toml` sets `purge-unused-after` for its own
|
|
437
|
+
packages.
|
|
438
|
+
* **No duplication**:
|
|
439
|
+
Programs that need the same package share one copy.
|
|
440
|
+
* **Versions side by side**:
|
|
441
|
+
Programs that need different versions of a package each get the correct one.
|
|
442
|
+
* **One version per program**:
|
|
443
|
+
A running program can import only one version of a package, so two
|
|
444
|
+
parts of one program cannot use different versions of the same one.
|
|
445
|
+
* **Where it has been tested**:
|
|
446
|
+
Linux, Windows and macOS (Intel and Apple Silicon) on GitHub's test
|
|
447
|
+
machines, Python 3.11 to 3.15; not yet tried with the Microsoft Store,
|
|
448
|
+
Homebrew or Apple's own Pythons.
|
|
449
|
+
|
|
450
|
+
## How `EZPkg` Actually Works
|
|
451
|
+
|
|
452
|
+
This section is for the curious-minded. Feel free to skip it unless you
|
|
453
|
+
are interested in puzzling out how the `EZPkg` bootstrap actually works
|
|
454
|
+
on your own.
|
|
455
|
+
|
|
456
|
+
Please open `quick_start.py` and go to `# Part 2: ...`. The entire
|
|
457
|
+
bootstrap runs inside a Python `try` block. Before each step, the
|
|
458
|
+
variable `s` is set to the step's name, so that if anything goes wrong,
|
|
459
|
+
the `except` at the bottom stops the program with
|
|
460
|
+
`ezpkg: <step> failed: ...`, naming the step that broke. The steps are:
|
|
461
|
+
* `s = "find ezpkg"`:
|
|
462
|
+
* This names `~/.ezpkg/ezpkg.whl`, where `ezpkg` is kept, and tries
|
|
463
|
+
`import ezpkg`. That succeeds when something like `uv run` has
|
|
464
|
+
already provided it.
|
|
465
|
+
* If `ezpkg` imported, or `~/.ezpkg/ezpkg.whl` already exists, the
|
|
466
|
+
download is skipped. It runs only the first time any program uses
|
|
467
|
+
`EZPkg` on a machine.
|
|
468
|
+
* `def g(u)`:
|
|
469
|
+
* This fetches a URL for the download. If Python cannot, it asks
|
|
470
|
+
`curl` instead. That happens with the Python installer from
|
|
471
|
+
python.org on macOS, which has no certificates (and so cannot check
|
|
472
|
+
that it is really talking to PyPI) until its
|
|
473
|
+
`Install Certificates.command` has been run. Every Mac has `curl`,
|
|
474
|
+
which uses the Mac's own certificates.
|
|
475
|
+
* `s = "download ezpkg"`:
|
|
476
|
+
* This asks [PyPI](https://pypi.org/) for the newest `ezpkg` wheel,
|
|
477
|
+
downloads it, checks its SHA-256 against the one PyPI publishes, and
|
|
478
|
+
keeps it as `~/.ezpkg/ezpkg.whl`, which is actually just a zip file.
|
|
479
|
+
* `s = "import ezpkg"`:
|
|
480
|
+
* This puts `~/.ezpkg/ezpkg.whl` at the front of `sys.path` and
|
|
481
|
+
imports `ezpkg` straight from it. Since Python can import pure
|
|
482
|
+
Python code from inside a zip file, nothing is actually installed.
|
|
483
|
+
* `s = "fetch packages"`:
|
|
484
|
+
* `m.EZPkg()` sets up the `~/.ezpkg` directory, and `.install()`
|
|
485
|
+
reads Part 1, downloads any packages not already there, and puts
|
|
486
|
+
them on `sys.path`, so that the imports in Part 3 succeed. (This is
|
|
487
|
+
why the user's import statements must be after the bootstrap.)
|
|
488
|
+
* `bootstrap="YYYY-MM-DD"` names the bootstrap this program carries.
|
|
489
|
+
They are always referred to by their published date.
|
|
490
|
+
* If Part 1's `ezpkg` entry asks for a different version, this `ezpkg`
|
|
491
|
+
fetches that one and restarts the program using it instead.
|
|
492
|
+
|
|
493
|
+
`ezpkg` fetches packages with `pip`, the standard Python tool. It downloads
|
|
494
|
+
its own copy of `pip` (the newest one your Python can run) and keeps it
|
|
495
|
+
as `~/.ezpkg/pip.whl`. This is because some commonly used Linux releases
|
|
496
|
+
(e.g. Ubuntu) do not include `pip` with the base Python installation and
|
|
497
|
+
other releases refuse to let it install into the system's Python
|
|
498
|
+
([PEP 668](https://peps.python.org/pep-0668/)). The machine's own
|
|
499
|
+
`pip`, if it has one, is never changed.
|
|
500
|
+
|
|
501
|
+
A release also uses [`uv`](https://docs.astral.sh/uv/), the fast package
|
|
502
|
+
manager written in Rust. `pip` only works out what *this* computer needs,
|
|
503
|
+
so a release asks `uv` to work out what every other computer needs too
|
|
504
|
+
(Windows, macOS, Linux, and each Python the program allows), held to the
|
|
505
|
+
versions your own runs approved. If `uv` is not installed, `ezpkg`
|
|
506
|
+
fetches its own copy into `~/.ezpkg`, just as it does for `pip`.
|
|
507
|
+
|
|
508
|
+
## Troubleshooting
|
|
509
|
+
|
|
510
|
+
These are the messages a developer is most likely to meet, and what to
|
|
511
|
+
do about each. An end user sees only the one-line failures in [Running a
|
|
512
|
+
Program That Uses `EZPkg`](#running-a-program-that-uses-ezpkg).
|
|
513
|
+
* **``PROG_NAME.py.toml line 3: unknown setting `relase` (did you mean
|
|
514
|
+
`release`?)``**:
|
|
515
|
+
Fix the spelling in `PROG_NAME.py.toml`; the program does not run until
|
|
516
|
+
every setting is known.
|
|
517
|
+
* **``PROG_NAME.py.toml lines LINENO1 and LINENO2: `update-frequency` is set
|
|
518
|
+
twice; keep only one of them``**:
|
|
519
|
+
The `#` was removed from two lines of one setting. Put it back on the
|
|
520
|
+
one you did not mean. (The message gives the actual line numbers.)
|
|
521
|
+
* **``PROG_NAME.py line 7: `import tabulat` needs a package, but PyPI has none
|
|
522
|
+
called `tabulat` ``**:
|
|
523
|
+
Usually a misspelt import. If the package's name on PyPI differs from
|
|
524
|
+
the module's, add the right name to `dependencies` yourself.
|
|
525
|
+
* **``line 12: `cowsay` is imported above the bootstrap; move it below,
|
|
526
|
+
with the other imports``**:
|
|
527
|
+
On a computer whose `~/.ezpkg` is still empty, that import runs before
|
|
528
|
+
anything has been fetched, so it fails there even though it works for
|
|
529
|
+
you.
|
|
530
|
+
* **An edited bootstrap**:
|
|
531
|
+
`EZPkg` stops, showing each line that differs from the published
|
|
532
|
+
bootstrap, with a `|` under the first different character. Paste the
|
|
533
|
+
bootstrap again from `quick_start.py`.
|
|
534
|
+
* **``PROG_NAME.py line 14 imports `PKG_NAME`, which is newer than
|
|
535
|
+
YYYY-MM-DD, so PROG_NAME.py moves to today's versions ...``**:
|
|
536
|
+
Nothing to do. Your versions were frozen at the date of your last update
|
|
537
|
+
(see `update-frequency`), and `PKG_NAME` is newer than that, so `EZPkg`
|
|
538
|
+
moved this program, and only this program, to today's versions of all
|
|
539
|
+
its packages. The next release carries them.
|
|
540
|
+
* **``PROG_NAME.py line 14 imports `PKG_NAME`, but no release of it existed
|
|
541
|
+
before the date ... (line 5, YYYY-MM-DD) ...``**:
|
|
542
|
+
Part 1 of `PROG_NAME.py` has an `exclude-newer` date of your own, which
|
|
543
|
+
`EZPkg` never changes. Delete that line, as the message says, to move
|
|
544
|
+
everything to today's versions.
|
|
545
|
+
* **``PROG_NAME.py 1.0.1 was not released: ...``**:
|
|
546
|
+
The release stopped for the reason given; `PROG_NAME.py` still runs, and the
|
|
547
|
+
next run tries the release again, so fix the problem and rerun it.
|
|
548
|
+
|
|
549
|
+
## Project Information
|
|
550
|
+
|
|
551
|
+
* **Source**:
|
|
552
|
+
[`ezpkg`](https://github.com/waynegramlich/ezpkg)
|
|
553
|
+
* **How it was built**:
|
|
554
|
+
`EZPkg` was developed primarily with
|
|
555
|
+
[Claude Code](https://claude.com/claude-code), Anthropic's coding agent,
|
|
556
|
+
using the Claude Opus 5.5 model, with the author directing the design
|
|
557
|
+
and the documentation. Most of the effort went into testing: the test
|
|
558
|
+
suite and its tools (coverage, scripted first-run sessions, and checks
|
|
559
|
+
on GitHub's Linux, Windows and macOS machines) are about 13,000 lines,
|
|
560
|
+
against about 9,600 for `ezpkg.py` itself.
|
|
561
|
+
* **Bug Reports and Feature Requests**:
|
|
562
|
+
[EZPkg Issues](https://github.com/waynegramlich/ezpkg/issues) is read very
|
|
563
|
+
infrequently by the principal author. A much better strategy is to use
|
|
564
|
+
the standard GitHub cycle: Fork, Clone, Commit, and Pull Request. A pull
|
|
565
|
+
request must pass the associated test suites without decreasing the code
|
|
566
|
+
coverage percentage before it will even remotely be considered for a
|
|
567
|
+
merge. How to run the tests, coverage and the GitHub Actions checks is
|
|
568
|
+
explained at the top of
|
|
569
|
+
[`ezpkg_test.py`](https://github.com/waynegramlich/ezpkg/blob/main/ezpkg_test.py).
|
|
570
|
+
* **License**: MIT No Attribution (`MIT-0`); see
|
|
571
|
+
[`LICENSE`](https://github.com/waynegramlich/ezpkg/blob/main/LICENSE).
|
|
572
|
+
|
|
573
|
+
## Conclusion
|
|
574
|
+
|
|
575
|
+
`EZPkg` does all of the package downloading and dependency checking for you.
|
|
576
|
+
When you are done, you can literally email someone one of the following
|
|
577
|
+
results:
|
|
578
|
+
* **`email`**:
|
|
579
|
+
A single `.py` file with the `EZPkg` bootstrap embedded in it. The
|
|
580
|
+
resulting program needs network access to download needed packages the
|
|
581
|
+
first time. After they are downloaded, the network is no longer needed.
|
|
582
|
+
* **`zipapp`**:
|
|
583
|
+
A single `.pyz` file that can be directly executed by a Python interpreter.
|
|
584
|
+
`zipapp` requires that all packages be pure Python without any packages
|
|
585
|
+
that contain non-Python code (e.g. C, C++, Rust). No network access
|
|
586
|
+
is needed for a `zipapp` `.pyz` file.
|
|
587
|
+
* **`uv`**:
|
|
588
|
+
In addition to having `uv` installed, the user needs two files:
|
|
589
|
+
* `PROG_NAME-VERSION-uv.py`, the program, run with `uv run`.
|
|
590
|
+
* `PROG_NAME-VERSION-uv.py.lock`, which pins every package's version
|
|
591
|
+
and checksum.
|
|
592
|
+
|
|
593
|
+
Note that `uv` needs network access for the first run to download packages.
|
|
594
|
+
|
|
595
|
+
All of the remaining hassle of managing packages is handled by `EZPkg`.
|