Skip to content

Commit 5c6b0c3

Browse files
authored
Merge pull request #127 from Falldog/fix/issue-124-unicode-argv
fix: issue 124 unicode sys.argv
2 parents dbfd9c7 + 663350d commit 5c6b0c3

File tree

3 files changed

+208
-32
lines changed

3 files changed

+208
-32
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Pyconcrete Changelog
22

3+
## 1.1.? (????)
4+
5+
### Bug fixes
6+
* Add `__file__` for entry pye script https://github.com/Falldog/pyconcrete/issues/123
7+
* Fix `sys.argv` to support unicode https://github.com/Falldog/pyconcrete/issues/124
8+
* Python 3.7 on Windows is not support
9+
10+
### Breaking Changes
11+
* Default enable Python utf8 mode, ref doc: https://docs.python.org/3/library/os.html#utf8-mode
12+
13+
314
## 1.1.0 (2025-07-24)
415

516
### Features

src/pyconcrete_exe/pyconcrete_exe.c

Lines changed: 171 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,52 @@
1818
#define MAGIC_OFFSET 8
1919
#endif
2020

21+
// WIN32 platform use wmain, all string related functions should change to wchar_t version
22+
#ifdef WIN32
23+
#define _CHAR wchar_t
24+
#define _T(s) L##s
25+
#define _fopen _wfopen
26+
#define _strncmp wcsncmp
27+
#define _strlen wcslen
28+
#define _PyConfig_SetArgv PyConfig_SetArgv
29+
#define _PyConfig_SetString PyConfig_SetString
30+
#define _PyUnicode_FromStringAndSize PyUnicode_FromWideChar
31+
#else
32+
#define _CHAR char
33+
#define _T(s) s
34+
#define _fopen fopen
35+
#define _strncmp strncmp
36+
#define _strlen strlen
37+
#define _PyConfig_SetArgv PyConfig_SetBytesArgv
38+
#define _PyConfig_SetString PyConfig_SetBytesString
39+
#define _PyUnicode_FromStringAndSize PyUnicode_FromStringAndSize
40+
#endif
41+
2142

2243
int createAndInitPyconcreteModule();
23-
int execPycContent(PyObject* pyc_content, const char* filepath);
24-
int runFile(const char* filepath);
25-
PyObject* getFullPath(const char* filepath);
44+
int execPycContent(PyObject* pyc_content, const _CHAR* filepath);
45+
int runFile(const _CHAR* filepath);
46+
int prependSysPath0(const _CHAR* script_path);
47+
void initPython(int argc, _CHAR *argv[]);
48+
PyObject* getFullPath(const _CHAR* filepath);
2649

2750

51+
#ifdef WIN32
52+
int wmain(int argc, wchar_t *argv[])
53+
#else
2854
int main(int argc, char *argv[])
55+
#endif
2956
{
30-
#if PY_MAJOR_VERSION >= 3
31-
int i, len;
3257
int ret = RET_OK;
33-
wchar_t** argv_ex = NULL;
34-
argv_ex = (wchar_t**) malloc(sizeof(wchar_t*) * argc);
35-
for(i=0 ; i<argc ; ++i)
36-
{
37-
len = mbstowcs(NULL, argv[i], 0);
38-
argv_ex[i] = (wchar_t*) malloc(sizeof(wchar_t) * (len+1));
39-
mbstowcs(argv_ex[i], argv[i], len);
40-
argv_ex[i][len] = 0;
41-
}
42-
#else
43-
char** argv_ex = argv;
44-
#endif
4558

46-
Py_SetProgramName(argv_ex[0]); /* optional but recommended */
4759
// PyImport_AppendInittab must set up before Py_Initialize
4860
if (PyImport_AppendInittab("_pyconcrete", PyInit__pyconcrete) == -1)
4961
{
5062
fprintf(stderr, "Error, can't load embedded _pyconcrete correctly!\n");
5163
return RET_FAIL;
5264
}
65+
66+
initPython(argc, argv);
5367
Py_Initialize();
5468
PyGILState_Ensure();
5569

@@ -62,13 +76,40 @@ int main(int argc, char *argv[])
6276

6377
if(argc >= 2)
6478
{
65-
if(argc == 2 && (strncmp(argv[1], "-v", 3)==0 || strncmp(argv[1], "--version", 10)==0))
79+
if(argc == 2 && (_strncmp(argv[1], _T("-v"), 3)==0 || _strncmp(argv[1], _T("--version"), 10)==0))
6680
{
6781
printf("pyconcrete %s [Python %s]\n", TOSTRING(PYCONCRETE_VERSION), TOSTRING(PY_VERSION)); // defined by build-backend
6882
}
6983
else
7084
{
85+
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION <=7
86+
#if defined(WIN32)
87+
PySys_SetArgv(argc-1, argv+1);
88+
#else
89+
int i, len;
90+
wchar_t** argv_ex = NULL;
91+
argv_ex = (wchar_t**) malloc(sizeof(wchar_t*) * argc);
92+
// setup
93+
for(i=0 ; i<argc ; ++i)
94+
{
95+
len = mbstowcs(NULL, argv[i], 0);
96+
argv_ex[i] = (wchar_t*) malloc(sizeof(wchar_t) * (len+1));
97+
mbstowcs(argv_ex[i], argv[i], len);
98+
argv_ex[i][len] = 0;
99+
}
100+
101+
// set argv
71102
PySys_SetArgv(argc-1, argv_ex+1);
103+
104+
// release
105+
for(i=0 ; i<argc ; ++i)
106+
{
107+
free(argv_ex[i]);
108+
}
109+
#endif
110+
#else
111+
prependSysPath0(argv[1]);
112+
#endif // PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION <=7
72113
ret = runFile(argv[1]);
73114
}
74115
}
@@ -91,17 +132,79 @@ int main(int argc, char *argv[])
91132
}
92133

93134
Py_Finalize();
135+
return ret;
136+
}
94137

95-
#if PY_MAJOR_VERSION >= 3
96-
for(i=0 ; i<argc ; ++i)
138+
139+
void initPython(int argc, _CHAR *argv[]) {
140+
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION <=7
141+
#if defined(WIN32)
142+
Py_SetProgramName(argv[0]);
143+
#else
144+
int len = mbstowcs(NULL, argv[0], 0);
145+
wchar_t* arg0 = (wchar_t*) malloc(sizeof(wchar_t) * (len+1));
146+
mbstowcs(arg0, argv[0], len);
147+
arg0[len] = 0;
148+
Py_SetProgramName(arg0);
149+
#endif
150+
#else
151+
PyStatus status;
152+
153+
// ----------
154+
// PyPreConfig
155+
// ----------
156+
// On Windows platform invoke pyconcrete by subprocess may changed the console encoding to cp1252
157+
// force to set utf8 mode to avoid the issue.
158+
PyPreConfig preconfig;
159+
PyPreConfig_InitPythonConfig(&preconfig);
160+
preconfig.utf8_mode = 1;
161+
162+
status = Py_PreInitialize(&preconfig);
163+
if (PyStatus_Exception(status)) {
164+
goto INIT_EXCEPTION;
165+
}
166+
167+
// ----------
168+
// PyConfig
169+
// ----------
170+
PyConfig config;
171+
PyConfig_InitPythonConfig(&config);
172+
config.parse_argv = 0;
173+
config.isolated = 1;
174+
175+
// Set program_name as pyconcrete. (Implicitly preinitialize Python)
176+
status = _PyConfig_SetString(&config, &config.program_name, argv[0]);
177+
if (PyStatus_Exception(status)) {
178+
goto INIT_EXCEPTION;
179+
}
180+
181+
// Decode command line arguments. (Implicitly preinitialize Python)
182+
status = _PyConfig_SetArgv(&config, argc-1, argv+1);
183+
if (PyStatus_Exception(status))
97184
{
98-
free(argv_ex[i]);
185+
goto INIT_EXCEPTION;
99186
}
100-
free(argv_ex);
101-
#endif
102-
return ret;
187+
188+
status = Py_InitializeFromConfig(&config);
189+
if (PyStatus_Exception(status))
190+
{
191+
goto INIT_EXCEPTION;
192+
}
193+
PyConfig_Clear(&config);
194+
return;
195+
196+
INIT_EXCEPTION:
197+
PyConfig_Clear(&config);
198+
if (PyStatus_IsExit(status))
199+
{
200+
return status.exitcode;
201+
}
202+
// Display the error message and exit the process with non-zero exit code
203+
Py_ExitStatusException(status);
204+
#endif // ifdef PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION <=7
103205
}
104206

207+
105208
int createAndInitPyconcreteModule()
106209
{
107210
int ret = 0;
@@ -137,7 +240,8 @@ int createAndInitPyconcreteModule()
137240
return ret;
138241
}
139242

140-
int execPycContent(PyObject* pyc_content, const char* filepath)
243+
244+
int execPycContent(PyObject* pyc_content, const _CHAR* filepath)
141245
{
142246
int ret = RET_OK;
143247
PyObject* py_marshal = NULL;
@@ -186,7 +290,8 @@ int execPycContent(PyObject* pyc_content, const char* filepath)
186290
return ret;
187291
}
188292

189-
int runFile(const char* filepath)
293+
294+
int runFile(const _CHAR* filepath)
190295
{
191296
FILE* src = NULL;
192297
char* content = NULL;
@@ -196,7 +301,7 @@ int runFile(const char* filepath)
196301
PyObject* py_plaint_content = NULL;
197302
PyObject* py_args = NULL;
198303

199-
src = fopen(filepath, "rb");
304+
src = _fopen(filepath, _T("rb"));
200305
if(src == NULL)
201306
{
202307
return RET_FAIL;
@@ -230,17 +335,51 @@ int runFile(const char* filepath)
230335
return ret;
231336
}
232337

233-
PyObject* getFullPath(const char* filepath)
338+
339+
/*
340+
PySys_SetArgv is deprecated since python 3.11. It's original behavior will insert script's directory into sys.path.
341+
It's replace by PyConfig, but PyConfig only update sys.path when executing Py_Main or Py_RunMain.
342+
So it's better to update sys.path by pyconcrete.
343+
*/
344+
int prependSysPath0(const _CHAR* script_path)
345+
{
346+
// script_dir = os.path.dirname(script_path)
347+
// sys.path.insert(0, script_dir)
348+
int ret = RET_OK;
349+
350+
PyObject* py_script_path = getFullPath(script_path);
351+
PyObject* path_module = PyImport_ImportModule("os.path");
352+
PyObject* dirname_func = PyObject_GetAttrString(path_module, "dirname");
353+
PyObject* args = Py_BuildValue("(O)", py_script_path);
354+
PyObject* script_dir = PyObject_CallObject(dirname_func, args);
355+
356+
PyObject* sys_path = PySys_GetObject("path");
357+
if (PyList_Insert(sys_path, 0, script_dir) < 0) {
358+
ret = RET_FAIL;
359+
}
360+
361+
Py_XDECREF(py_script_path);
362+
Py_XDECREF(path_module);
363+
Py_XDECREF(dirname_func);
364+
Py_XDECREF(args);
365+
Py_XDECREF(script_dir);
366+
return ret;
367+
}
368+
369+
370+
PyObject* getFullPath(const _CHAR* filepath)
234371
{
235372
// import os.path
236373
// return os.path.abspath(filepath)
237374
PyObject* path_module = PyImport_ImportModule("os.path");
238375
PyObject* abspath_func = PyObject_GetAttrString(path_module, "abspath");
239-
PyObject* args = Py_BuildValue("(s)", filepath);
240-
PyObject* obj = PyObject_CallObject(abspath_func, args);
376+
PyObject* py_filepath = _PyUnicode_FromStringAndSize(filepath, _strlen(filepath));
377+
PyObject* args = Py_BuildValue("(O)", py_filepath);
378+
PyObject* py_file_abspath = PyObject_CallObject(abspath_func, args);
241379

242380
Py_XDECREF(path_module);
243381
Py_XDECREF(abspath_func);
382+
Py_XDECREF(py_filepath);
244383
Py_XDECREF(args);
245-
return obj;
384+
return py_file_abspath;
246385
}

tests/test_exe.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import os.path
15+
import platform
1516
import subprocess
17+
import sys
18+
19+
import pytest
1620

1721

1822
def test_exe__execute_an_non_exist_file(venv_exe):
@@ -85,6 +89,28 @@ def test_exe__sys_argv__more_arguments(venv_exe, pye_cli, tmpdir):
8589
assert output == f'{pye_path} -a -b -c'
8690

8791

92+
@pytest.mark.skipif(platform.system() == 'Windows' and sys.version_info < (3, 8), reason="Windows requires python3.8")
93+
def test_exe__sys_argv__in_unicode(venv_exe, pye_cli, tmpdir):
94+
# prepare
95+
pye_path = (
96+
pye_cli.setup(tmpdir, 'test_sys_argv')
97+
.source_code(
98+
"""
99+
import sys
100+
print(" ".join(sys.argv))
101+
""".strip()
102+
)
103+
.get_encrypt_path()
104+
)
105+
106+
# execution
107+
output = venv_exe.pyconcrete(pye_path, '早安', '=', 'おはようございます')
108+
output = output.strip()
109+
110+
# verification
111+
assert output == f'{pye_path} 早安 = おはようございます'
112+
113+
88114
def test_exe__import_pyconcrete__venv_exe__validate__file__(venv_exe, pye_cli, tmpdir):
89115
"""
90116
compare to test_lib__import_pyconcrete__venv_lib__validate__file__

0 commit comments

Comments
 (0)