Skip to content

Commit c6e4d49

Browse files
committed
Support running in file or command mode
1 parent 02b14b4 commit c6e4d49

4 files changed

Lines changed: 72 additions & 15 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ Custom (python) import hooks are installed to support loading modules (both nati
1616

1717
Currently the exe just drops to an interactive loop. Modification to run a script or embed a file/module should be trivial.
1818

19-
- [ ] Support `onefile_python.exe script.py`
2019
- [ ] Build option for embedding a python file/module and running that on launch
2120
- [ ] Support other versions of python than `3.10.1` (autodetect?)
2221

@@ -32,7 +31,7 @@ py2exe supports a diskless/"bundle" mode.
3231

3332
Both these projects are *much* more complex than this one and support lots of extra features, but sometimes you don't need that...
3433

35-
This project is (maybe) better if you want a single exe that can run any python script [or will be once that is supported], or just want an exe that gives a python REPL.
34+
This project is (maybe) better if you want a single exe that can run any python script, or just want an exe that gives a python REPL.
3635
Being simpler, this project should be easier to hack on or learn from.
3736

3837

@@ -42,7 +41,7 @@ There's not much to it...
4241

4342
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.
4443
1. Use [zippy](https://github.com/guzba/zippy) to access the contents of the archive at runtime.
45-
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.
44+
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.
4645
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)
4746
4. Use [nimpy](https://github.com/yglukhov/nimpy) to initialize a python extension exporting some nim functions that can read data out of the `python*.zip` standard library (contained within the `...-embedded.zip`).
4847
5. Run the embedded `bootsrap.py` code to install an import hook. This import hook uses the functions from (4) to support importing python modules. If a `.pyc` can be found that matches an import, a loader that returns the unmarshalled `.pyc` is provided. If a `.pyd` can be found, the returned loader reflectively loads the `.pyd` and calls the module's initialization routine.

bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import _frozen_importlib_external as _bootstrap_external
55
import _frozen_importlib as _bootstrap
66

7-
from nimporter import *
7+
from onefile_python import *
88

99
_bootstrap._install_external_importers()
1010
_bootstrap_external._set_bootstrap_module(_bootstrap)

onefile_python.nim

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import std/strutils
77
import zippy/ziparchives
88
import minhook
99
import winim/lean
10-
1110
import memlib
1211
import nimpy
1312
import nimpy/py_types
13+
import argparse
1414

1515
import cpython_types
1616

@@ -19,6 +19,14 @@ 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

22+
let p = newParser("onefile_python"):
23+
help("{prog}")
24+
flag("-V", "--version")
25+
option("-c", "--command", help="program passed in as string")
26+
arg("file", default=some(""), help="program read from script file")
27+
arg("arg", nargs=(-1), help="arguments passed to program in sys.argv[1:]")
28+
29+
2230
template `//`(a, b: untyped) : untyped = a div b
2331

2432
converter falsey(i: int): bool = i != 0
@@ -73,13 +81,21 @@ proc Py_SetProgramName(name: pointer): void {. memlib: python, importc: "Py_SetP
7381
proc Py_SetPath(path: pointer): void {. memlib: python, importc: "Py_SetPath", cdecl .}
7482
# proc PyConfig_InitPythonConfig(config: ptr PyConfig): void {. memlib: python, importc: "PyConfig_InitPythonConfig", cdecl .}
7583
proc PyConfig_InitIsolatedConfig(config: ptr PyConfig): void {. memlib: python, importc: "PyConfig_InitIsolatedConfig", cdecl .}
84+
proc PyConfig_SetArgv(r: PyStatus, config: ptr PyConfig, argc: int, argv: pointer): ptr PyStatus {. memlib: python, importc: "PyConfig_SetArgv", cdecl .}
7685
proc Py_InitializeFromConfig(r: PyStatus, config: ptr PyConfig): ptr PyStatus {. memlib: python, importc: "Py_InitializeFromConfig", cdecl .}
7786
proc PyUnicode_AsUTF8AndSize(obj: pointer, size: pointer): cstring {. memlib: python, importc: "PyUnicode_AsUTF8AndSize", cdecl .}
78-
proc PyRun_SimpleString(code: cstring): int {. memlib: python, importc: "PyRun_SimpleString", cdecl .}
7987
proc PyUnicode_FromString(str: cstring): pointer {. memlib: python, importc: "PyUnicode_FromString", cdecl .}
8088
proc PyImport_GetModuleDict(): pointer {. memlib: python, importc: "PyImport_GetModuleDict", cdecl .}
8189
proc PyImport_FixupExtensionObject(module: pointer, name: pointer, filename: pointer, modules: pointer): void {. memlib: python, importc: "_PyImport_FixupExtensionObject", cdecl .}
8290
proc PyModule_FromDefAndSpec2(def: PPyObject, spec: PPyObject, module: int): PPyObject {. memlib: python, importc: "PyModule_FromDefAndSpec2", cdecl .}
91+
92+
proc Py_BuildValue(v1: cstring, v2: cstring): pointer {. memlib: python, importc: "Py_BuildValue", cdecl .}
93+
proc Py_fopen_obj(path: pointer, mode: cstring): pointer {. memlib: python, importc: "_Py_fopen_obj", cdecl .}
94+
proc PyRun_SimpleFile(fp: pointer, filename: cstring): cint {. memlib: python, importc: "PyRun_SimpleFile", cdecl .}
95+
96+
proc Py_GetVersion(): cstring {. memlib: python, importc: "Py_GetVersion", cdecl .}
97+
98+
proc PyRun_SimpleString(code: cstring): int {. memlib: python, importc: "PyRun_SimpleString", cdecl .}
8399
proc PyRun_InteractiveLoop(file: pointer, filename: cstring): int {. memlib: python, importc: "PyRun_InteractiveLoop", cdecl .}
84100
proc PyObject_Repr(obj: pointer): cstring {. memlib: python, importc: "PyObject_Repr", cdecl .}
85101
proc Py_InitializeMain(r: ptr PyStatus): ptr PyStatus {. memlib: python, importc: "_Py_InitializeMain", cdecl .}
@@ -138,11 +154,26 @@ proc PyInit_onefile_python(): pointer {. stdcall, importc: "PyInit_onefile_pytho
138154
const bootstrap_py = staticRead("bootstrap.py")
139155

140156

157+
proc parse_args: auto =
158+
try:
159+
return p.parse(commandLineParams())
160+
except ShortCircuit as e:
161+
if e.flag == "argparse_help":
162+
echo p.help
163+
quit(1)
164+
except UsageError:
165+
stderr.writeLine getCurrentExceptionMsg()
166+
quit(1)
167+
168+
141169
proc main =
142170
var r: PyStatus
143171
var status: ptr PyStatus
144172
var res: int
145173

174+
var opts = parse_args()
175+
176+
146177
# Initial configuration - ignore argv, env vars (PYTHONHOME), any files, etc.
147178
var preconfig: PyPreConfig
148179
PyPreConfig_InitIsolatedConfig(preconfig)
@@ -164,17 +195,34 @@ proc main =
164195
config.init_main = 0
165196
when DEBUG:
166197
echo fmt"Py_InitializeFromConfig({config})"
198+
199+
var argv: array[256, pointer]
200+
var argc = 0
201+
if opts.file != "":
202+
argv[argc] = Py_DecodeLocale(opts.file.cstring, NULL)
203+
argc += 1
204+
for arg in opts.arg:
205+
argv[argc] = Py_DecodeLocale(arg.cstring, NULL)
206+
argc += 1
207+
if argc >= 256:
208+
break
209+
discard PyConfig_SetArgv(r, config, argc, argv.addr)
210+
167211
status = Py_InitializeFromConfig(r, config)
168212
if PyStatus_Exception(status):
169213
Py_ExitStatusException(status)
170214
return
171215

216+
if opts.version:
217+
echo $Py_GetVersion()
218+
quit()
219+
172220
# Set up "nimporter" to expose pyd_has, pyd_load, stdlib_has, stdlib_read
173221
when DEBUG:
174-
echo "Loading nimporter module"
222+
echo "Loading onefile_python python module"
175223
let modules = PyImport_GetModuleDict()
176224
let own_module = PyInit_onefile_python()
177-
PyImport_FixupExtensionObject(own_module, PyUnicode_FromString("nimporter"), PyUnicode_FromString("<memory>"), modules)
225+
PyImport_FixupExtensionObject(own_module, PyUnicode_FromString("onefile_python"), PyUnicode_FromString("<memory>"), modules)
178226

179227
# Run `bootstrap.py` to install import hooks
180228
when DEBUG:
@@ -194,16 +242,24 @@ proc main =
194242

195243
# Load dlls in the embedded archive. This seems to trigger AV if we call the nim function directly, but calling it via python is enough to bypass...
196244
res = PyRun_SimpleString("""
197-
import nimporter
198-
nimporter.load_dlls()
245+
import onefile_python
246+
onefile_python.load_dlls()
199247
""")
200248
if res > 0:
201249
echo "Error running nimporter.load_dlls()"
202250
return
203251

204-
# TODO parse cmdline and run .py, OR do loop, (or maybe run __autorun__.py)
205-
# MAYBE: clean up scope from vars in bootstrap.py
206-
discard PyRun_InteractiveLoop(stdin, "stdin")
207-
252+
# run modes: command, TODO: module, file, interactive
253+
if opts.command != "":
254+
discard PyRun_SimpleString(opts.command.cstring)
255+
elif opts.file == "-" or opts.file == "":
256+
discard PyRun_InteractiveLoop(stdin, "stdin")
257+
elif opts.file != "-":
258+
let filename_str = Py_BuildValue("s", opts.file.cstring)
259+
let file = Py_fopen_obj(filename_str, "rb")
260+
if file != NULL:
261+
discard PyRun_SimpleFile(file, opts.file.cstring)
262+
else:
263+
echo fmt"can't open file '{opts.file}'"
208264

209265
main()

onefile_python.nimble

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

3-
version = "0.1.0"
3+
version = "0.2.0"
44
author = "Simon Pinfold"
55
description = "Run python from a single exe"
66
license = "ISC"
@@ -12,6 +12,8 @@ requires "nim >= 1.6.2"
1212
requires "https://github.com/synap5e/memlib.git" # Use fork while https://github.com/khchen/memlib/pull/3 is not merged
1313
requires "zippy >= 0.7.3"
1414
requires "nimpy >= 0.2.0"
15+
requires "nimpy >= 0.2.0"
16+
requires "https://github.com/iffy/nim-argparse"
1517

1618
task build, "Build":
1719
exec "nim -d:release c onefile_python.nim"

0 commit comments

Comments
 (0)