Skip to content

Commit b370473

Browse files
committed
Add option to build in "staged" mode
Instead of embedding the .zip, onefile_python.exe will download the python-embedded zip on each run. Download URL can be set through flags or the .exe name.
1 parent 2bb2529 commit b370473

3 files changed

Lines changed: 71 additions & 8 deletions

File tree

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,63 @@
22

33
Run python from a single exe (without needing to extract anything to disk).
44

5-
This project uses reflective dll loading and nim's `staticRead` to load the python runtime from the executable itself.
5+
This project uses reflective dll loading and either nim's `staticRead` to load the python runtime from the executable itself, or optionally downloads the embedded zip on launch ("staged" mode).
66
Custom (python) import hooks are installed to support loading modules (both native python (.pyc) and extension modules (.pyd)) from the embedded standard library.
77

8-
## HOWTO
8+
## Building
9+
10+
Or just download from from the [releases page](https://github.com/synap5e/onefile_python/releases).
911

1012
0. Set up nim
13+
14+
### Zip embedded in file
15+
1116
1. Download `python-3.10.1-embed-amd64.zip` to the project directory
1217
2. `nimble build`
1318
3. Run `onefile_python.exe`
1419

15-
## TODO
20+
### Download zip on launch ("staged")
21+
22+
Potentially useful if you want a smaller exe.
23+
24+
1. `nimble build -d:staged`
25+
2. Run `onefile_python.exe`
26+
27+
28+
## Usage
29+
30+
```
31+
onefile_python
1632
17-
Currently the exe just drops to an interactive loop. Modification to run a script or embed a file/module should be trivial.
33+
Usage:
34+
onefile_python [options] [file] [arg ...]
1835
19-
- [ ] Build option for embedding a python file/module and running that on launch
36+
Arguments:
37+
[file] Program; read from script file/URL ('-' or empty for interactive) (default: )
38+
[arg ...] Arguments passed to program in sys.argv[1:]
39+
40+
Options:
41+
-h, --help
42+
-V, --version
43+
-c, --command=COMMAND Program; passed in as string
44+
```
45+
`file` can be a http or https URL.
46+
47+
For staged version:
48+
```
49+
-d, --download=DOWNLOAD Download `python-3.10.1-embed-amd64.zip` from this url (default: https://www.python.org/ftp/python/3.10.1)
50+
51+
```
52+
53+
Alternatively specify the download URL in the app filename e.g. rename `onefile_python.exe` to `blabla(10.0.0.1)foobar.exe` to download python from `https://10.0.0.1/python-3.10.1-embed-amd64.zip`. `blabla` and `foobar` can be any string.
54+
55+
56+
## TODO
57+
58+
- [ ] Build option for embedding a python file/module and running that on launch (instead of accepting file/interactive loop)
2059
- [ ] Support other versions of python than `3.10.1` (autodetect?)
2160

61+
2262
## Similar projects
2363

2464
- [py2exe](https://www.py2exe.org/index.cgi/FrontPage)
@@ -39,7 +79,7 @@ Being simpler, this project should be easier to hack on or learn from.
3979

4080
There's not much to it...
4181

42-
0. Use nim's [staticRead](https://nim-lang.org/docs/system.html#staticRead%2Cstring) to include `python-*-embedded.zip` and `bootstrap.py` inside compiled exe itself.
82+
0. Use nim's [staticRead](https://nim-lang.org/docs/system.html#staticRead%2Cstring) to include `python-*-embedded.zip` and `bootstrap.py` inside compiled exe itself OR download the zip from a URL.
4383
1. Use [zippy](https://github.com/guzba/zippy) to access the contents of the archive at runtime.
4484
2. Use [memlib](https://github.com/khchen/memlib) to perform reflective dll loading of the embedded `python*.dll`. Reflective dll loading allows for loading the dll from memory rather than from disk. Hook `LdrLoadDll` and `K32EnumProcessModules` so other code using the dll can find it. n.b. currently using a fork until https://github.com/khchen/memlib/pull/3 is merged.
4585
3. Call various functions in the (reflectively) loaded dll to partially initialize python. Configure python to not try to load anything from disk (not absolutely required, but prevents conflicts and means the exe doesn't run any code in the current directory)

config.nims

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
21
--gc:orc
32
--define:windows
3+
when defined(staged):
4+
--define:ssl
45
when defined(release):
56
--define:danger
67
--passL:"-Wl,--gc-sections,-flto,-s" # gc sections, strip, link-time optimization

onefile_python.nim

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,22 @@ const DEBUG = false # Print debug messages (in nim code, and in python loaders)
1919
const EMBED_DLL = true # Use the .dll embedded in the zip archive. If false, will require python310.dll to be in the PATH. If true, debugging may be harder as the dll is reflectively loaded
2020

2121
const PYTHON_EMBEDDED_FILE = "python-3.10.1-embed-amd64.zip"
22+
when defined(staged):
23+
const PYTHON_EMBEDDED_URL = "https://www.python.org/ftp/python/3.10.1"
24+
2225

2326
let arg_parser = newParser("onefile_python"):
2427
help("{prog}")
2528
flag("-V", "--version")
2629
option("-c", "--command", help="Program; passed in as string")
2730
arg("file", default=some(""), help="Program; read from script file/URL ('-' or empty for interactive)")
2831
arg("arg", nargs=(-1), help="Arguments passed to program in sys.argv[1:]")
32+
when defined(staged):
33+
option("-d", "--download", help=fmt"Download `{PYTHON_EMBEDDED_FILE}` from this url", default=some(PYTHON_EMBEDDED_URL))
34+
when defined(staged):
35+
const extra_help = fmt"Alternatively specify the download URL in the app filename e.g. `blabla(10.0.0.1)foobar.exe` means download from `https://10.0.0.1/{PYTHON_EMBEDDED_FILE}`."
36+
else:
37+
const extra_help = ""
2938

3039

3140
template `//`(a, b: untyped) : untyped = a div b
@@ -46,14 +55,27 @@ proc parse_args: auto =
4655
except ShortCircuit as e:
4756
if e.flag == "argparse_help":
4857
echo arg_parser.help
58+
echo extra_help
4959
quit(1)
5060
except UsageError:
5161
stderr.writeLine getCurrentExceptionMsg()
5262
quit(1)
5363

5464
var opts = parse_args()
5565

56-
const python_310_embedded_zip = staticRead(PYTHON_EMBEDDED_FILE)
66+
when not defined(staged):
67+
const python_310_embedded_zip = staticRead(PYTHON_EMBEDDED_FILE)
68+
else:
69+
var downloadUrl = opts.download & "/" & PYTHON_EMBEDDED_FILE;
70+
let appFileName = getAppFilename().splitPath().tail;
71+
if appFileName.count("(") == 1 and appFileName.count(")") == 1 and appFileName.find(")") > appFileName.find("("):
72+
downloadUrl = "http://" & appFileName.substr(appFileName.find("(") + 1, appFileName.find(")") - 1) & "/" & PYTHON_EMBEDDED_FILE;
73+
74+
import std/httpclient
75+
let client = newHttpClient()
76+
echo fmt"Downloading from '{downloadUrl}'..."
77+
let python_310_embedded_zip = client.getContent(downloadUrl)
78+
echo "\tDone"
5779

5880
var python_embedded = ZipArchive()
5981
python_embedded.open(newStringStream(python_310_embedded_zip))

0 commit comments

Comments
 (0)