From 73f0f695204f6b5ac531de47d684b24afba26dad Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 16 Jun 2025 22:14:58 +0100 Subject: [PATCH 1/3] Ensure Start shortcuts and file associations use good working directory. For associations, this requires adding a (private) argument to the commands that will reinterpret the CWD. This argument is applied prior to launching the script, which may result in the script not being found. However, for the relevant cases, a full path is always passed for the script and this shouldn't happen. Fixes #134 --- _msbuild.py | 11 ++- _msbuild_test.py | 1 + src/_native/shortcut.cpp | 22 +++++ src/manage/commands.py | 2 + src/manage/startutils.py | 3 +- src/pymanager/appxmanifest.xml | 143 +++++++++++++++++---------------- src/pymanager/main.cpp | 43 ++++++++++ src/pymanager/msi.wxs | 14 ++-- 8 files changed, 158 insertions(+), 81 deletions(-) diff --git a/_msbuild.py b/_msbuild.py index 387a6cd..bebd623 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -78,6 +78,7 @@ class ResourceFile(CSourceFile): CFunction('file_locked_delete'), CFunction('package_get_root'), CFunction('shortcut_create'), + CFunction('shortcut_default_cwd'), CFunction('shortcut_get_start_programs'), CFunction('hide_file'), CFunction('fd_supports_vt100'), @@ -97,7 +98,10 @@ def main_exe(name): VersionInfo(FileDescription="Python Install Manager"), CPP_SETTINGS, ItemDefinition('ClCompile', PreprocessorDefinitions=Prepend(f'EXE_NAME=L"{name}";')), - ItemDefinition('Link', SubSystem='CONSOLE', DelayLoadDLLs=f"{DLL_NAME}.dll"), + ItemDefinition('Link', + SubSystem='CONSOLE', + DelayLoadDLLs=f'{DLL_NAME}.dll;ole32.dll;shell32.dll;advapi32.dll', + ), INCLUDE_TMPDIR, Manifest('default.manifest'), ResourceFile('pyicon.rc'), @@ -115,7 +119,10 @@ def mainw_exe(name): return CProject(name, VersionInfo(FileDescription="Python Install Manager (windowed)"), CPP_SETTINGS, - ItemDefinition('Link', SubSystem='WINDOWS', DelayLoadDLLs=f"{DLL_NAME}.dll"), + ItemDefinition('Link', + SubSystem='WINDOWS', + DelayLoadDLLs=f'{DLL_NAME}.dll;ole32.dll;shell32.dll;advapi32.dll', + ), INCLUDE_TMPDIR, ItemDefinition('ClCompile', PreprocessorDefinitions=Prepend(f'EXE_NAME=L"{name}";')), ItemDefinition('ClCompile', PreprocessorDefinitions=Prepend("PY_WINDOWED=1;")), diff --git a/_msbuild_test.py b/_msbuild_test.py index 5a3e71a..949a4ec 100644 --- a/_msbuild_test.py +++ b/_msbuild_test.py @@ -44,6 +44,7 @@ CFunction('file_locked_delete'), CFunction('package_get_root'), CFunction('shortcut_create'), + CFunction('shortcut_default_cwd'), CFunction('shortcut_get_start_programs'), CFunction('hide_file'), CFunction('fd_supports_vt100'), diff --git a/src/_native/shortcut.cpp b/src/_native/shortcut.cpp index 38c11bc..501f25c 100644 --- a/src/_native/shortcut.cpp +++ b/src/_native/shortcut.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "helpers.h" extern "C" { @@ -111,6 +112,27 @@ shortcut_get_start_programs(PyObject *, PyObject *, PyObject *) } +PyObject * +shortcut_default_cwd(PyObject *, PyObject *, PyObject *) +{ + static const KNOWNFOLDERID fids[] = { FOLDERID_Documents, FOLDERID_Profile }; + for (auto i = std::begin(fids); i != std::end(fids); ++i) { + wchar_t *path; + if (SUCCEEDED(SHGetKnownFolderPath( + *i, + KF_FLAG_NO_PACKAGE_REDIRECTION, + NULL, + &path + ))) { + PyObject *r = PyUnicode_FromWideChar(path, -1); + CoTaskMemFree(path); + return r; + } + } + return Py_GetConstant(Py_CONSTANT_NONE); +} + + PyObject * hide_file(PyObject *, PyObject *args, PyObject *kwargs) { diff --git a/src/manage/commands.py b/src/manage/commands.py index 1ef7724..b049b39 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -39,6 +39,8 @@ WELCOME = f"""!B!Python install manager was successfully updated to {__version__}.!W! +!Y!Start menu shortcuts have been changed in this update.!W! +Run !G!py install --refresh!W! to update any existing shortcuts. """ # The 'py help' or 'pymanager help' output is constructed by these default docs, diff --git a/src/manage/startutils.py b/src/manage/startutils.py index 2ca0b9f..4a45e7c 100644 --- a/src/manage/startutils.py +++ b/src/manage/startutils.py @@ -51,7 +51,8 @@ def _make(root, prefix, item, allow_warn=True): lnk, target, arguments=_unprefix(item.get("Arguments"), prefix), - working_directory=_unprefix(item.get("WorkingDirectory"), prefix), + working_directory=_unprefix(item.get("WorkingDirectory"), prefix) + or _native.shortcut_default_cwd(), icon=_unprefix(item.get("Icon"), prefix), icon_index=item.get("IconIndex", 0), ) diff --git a/src/pymanager/appxmanifest.xml b/src/pymanager/appxmanifest.xml index 959faec..efeb720 100644 --- a/src/pymanager/appxmanifest.xml +++ b/src/pymanager/appxmanifest.xml @@ -8,6 +8,7 @@ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:desktop7="http://schemas.microsoft.com/appx/manifest/desktop/windows10/7" + xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:uap13="http://schemas.microsoft.com/appx/manifest/uap/windows10/13" @@ -108,6 +109,53 @@ + + + Python Script + Python Script + _resources/pyx256.png + _resources/py.ico + + + .py + + + + + + Compiled Python Script + Compiled Python Script + _resources/pyc.ico + + + .pyc + + + + + + Python Extension Module + Python Extension Module + _resources/pyd.ico + + + .pyd + + + + + + Python Application + Python Application + _resources/pythonx150.png + _resources/python.ico + + + .pyz + + + + + + Python Script (Windowed) + Python Script (Windowed) + _resources/pyx256.png + _resources/py.ico + + + .pyw + + + + + + Python Application (Windowed) + Python Application (Windowed) + _resources/pythonwx150.png + _resources/pythonw.ico + + + .pyzw + + + @@ -147,53 +219,6 @@ - - - Python Script - Python Script - _resources/pyx256.png - _resources/py.ico - - - .py - - - - - - Compiled Python Script - Compiled Python Script - _resources/pyc.ico - - - .pyc - - - - - - Python Extension Module - Python Extension Module - _resources/pyd.ico - - - .pyd - - - - - - Python Application - Python Application - _resources/pythonx150.png - _resources/python.ico - - - .pyz - - - @@ -248,30 +273,6 @@ - - - Python Script (Windowed) - Python Script (Windowed) - _resources/pyx256.png - _resources/py.ico - - - .pyw - - - - - - Python Application (Windowed) - Python Application (Windowed) - _resources/pythonwx150.png - _resources/pythonw.ico - - - .pyzw - - - diff --git a/src/pymanager/main.cpp b/src/pymanager/main.cpp index 701e32a..7c7d556 100644 --- a/src/pymanager/main.cpp +++ b/src/pymanager/main.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -483,6 +484,36 @@ locate_runtime( } +static int +fix_working_directory(const std::wstring &script) +{ + HRESULT hr; + // If we have a script, use its parent directory + if (!script.empty()) { + auto end = script.find_last_of(L"/\\"); + if (end != script.npos) { + std::wstring current_dir(script.data(), end); + SetCurrentDirectoryW(current_dir.c_str()); + return 0; + } + } + // If we have no script, assume the user's documents folder + wchar_t *path; + if (SUCCEEDED(hr = SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &path))) { + SetCurrentDirectoryW(path); + CoTaskMemFree(path); + return 0; + } + // As a fallback, use the user's profile (e.g. for SYSTEM) + if (SUCCEEDED(hr = SHGetKnownFolderPath(FOLDERID_Profile, 0, NULL, &path))) { + SetCurrentDirectoryW(path); + CoTaskMemFree(path); + return 0; + } + return hr; +} + + int wmain(int argc, wchar_t **argv) { @@ -504,6 +535,7 @@ wmain(int argc, wchar_t **argv) const wchar_t *default_cmd; bool use_commands, use_cli_tag, use_shebangs, use_autoinstall; + bool fix_cwd = false; per_exe_settings(argc, argv, &default_cmd, &use_commands, &use_cli_tag, &use_shebangs, &use_autoinstall); if (use_commands) { @@ -519,6 +551,10 @@ wmain(int argc, wchar_t **argv) // We handle 'exec' in native code, so it won't be in the above list if (!wcscmp(argv[1], L"exec")) { skip_argc += 1; + if (!wcscmp(argv[2], L"--__fix-cwd")) { + fix_cwd = true; + skip_argc += 1; + } use_cli_tag = argc >= 3; use_shebangs = argc >= 3; default_cmd = NULL; @@ -570,6 +606,13 @@ wmain(int argc, wchar_t **argv) // Theoretically shouldn't matter, but might help reduce memory usage. close_python(); + if (fix_cwd) { + err = fix_working_directory(script); + if (err) { + fprintf(stderr, "[WARN] Failed to fix working directory (0x%08X).\n", err); + } + } + err = launch(executable.c_str(), args.c_str(), skip_argc, &exitCode); // TODO: Consider sharing print_error() with launcher.cpp diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index 320dfad..db66e4d 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -37,11 +37,11 @@ - + - + @@ -56,7 +56,7 @@ - + - + - + @@ -79,12 +79,12 @@ - + - + From 3fb030f178195c2884bed3d5e2b6e6fed2cdcda0 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 18 Jun 2025 19:29:32 +0100 Subject: [PATCH 2/3] Revert file association changes --- src/pymanager/appxmanifest.xml | 143 ++++++++++++++++----------------- src/pymanager/msi.wxs | 14 ++-- 2 files changed, 78 insertions(+), 79 deletions(-) diff --git a/src/pymanager/appxmanifest.xml b/src/pymanager/appxmanifest.xml index efeb720..959faec 100644 --- a/src/pymanager/appxmanifest.xml +++ b/src/pymanager/appxmanifest.xml @@ -8,7 +8,6 @@ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:desktop7="http://schemas.microsoft.com/appx/manifest/desktop/windows10/7" - xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:uap13="http://schemas.microsoft.com/appx/manifest/uap/windows10/13" @@ -109,53 +108,6 @@ - - - Python Script - Python Script - _resources/pyx256.png - _resources/py.ico - - - .py - - - - - - Compiled Python Script - Compiled Python Script - _resources/pyc.ico - - - .pyc - - - - - - Python Extension Module - Python Extension Module - _resources/pyd.ico - - - .pyd - - - - - - Python Application - Python Application - _resources/pythonx150.png - _resources/python.ico - - - .pyz - - - - - - Python Script (Windowed) - Python Script (Windowed) - _resources/pyx256.png - _resources/py.ico - - - .pyw - - - - - - Python Application (Windowed) - Python Application (Windowed) - _resources/pythonwx150.png - _resources/pythonw.ico - - - .pyzw - - - @@ -219,6 +147,53 @@ + + + Python Script + Python Script + _resources/pyx256.png + _resources/py.ico + + + .py + + + + + + Compiled Python Script + Compiled Python Script + _resources/pyc.ico + + + .pyc + + + + + + Python Extension Module + Python Extension Module + _resources/pyd.ico + + + .pyd + + + + + + Python Application + Python Application + _resources/pythonx150.png + _resources/python.ico + + + .pyz + + + @@ -273,6 +248,30 @@ + + + Python Script (Windowed) + Python Script (Windowed) + _resources/pyx256.png + _resources/py.ico + + + .pyw + + + + + + Python Application (Windowed) + Python Application (Windowed) + _resources/pythonwx150.png + _resources/pythonw.ico + + + .pyzw + + + diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index db66e4d..320dfad 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -37,11 +37,11 @@ - + - + @@ -56,7 +56,7 @@ - + - + - + @@ -79,12 +79,12 @@ - + - + From cc75e671d6ff91b3784c1b244ba22fd2b8bf8065 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 18 Jun 2025 19:50:06 +0100 Subject: [PATCH 3/3] Make --__fix-cwd optional and suppress unnecessary delay load warnings --- _msbuild.py | 2 ++ src/pymanager/main.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/_msbuild.py b/_msbuild.py index bebd623..9440010 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -101,6 +101,7 @@ def main_exe(name): ItemDefinition('Link', SubSystem='CONSOLE', DelayLoadDLLs=f'{DLL_NAME}.dll;ole32.dll;shell32.dll;advapi32.dll', + DisableSpecificWarnings=Prepend('4199;'), ), INCLUDE_TMPDIR, Manifest('default.manifest'), @@ -122,6 +123,7 @@ def mainw_exe(name): ItemDefinition('Link', SubSystem='WINDOWS', DelayLoadDLLs=f'{DLL_NAME}.dll;ole32.dll;shell32.dll;advapi32.dll', + DisableSpecificWarnings=Prepend('4199;'), ), INCLUDE_TMPDIR, ItemDefinition('ClCompile', PreprocessorDefinitions=Prepend(f'EXE_NAME=L"{name}";')), diff --git a/src/pymanager/main.cpp b/src/pymanager/main.cpp index 7c7d556..daa388d 100644 --- a/src/pymanager/main.cpp +++ b/src/pymanager/main.cpp @@ -24,6 +24,11 @@ #define PY_WINDOWED 0 #endif +// Uncomment to add the --__fix-cwd private argument, which will reset the +// working directory to the script location or user documents/profile before +// running Python. +//#define ENABLE_FIX_CWD + struct { PyObject *mod; PyObject *no_install_found_error; @@ -484,6 +489,7 @@ locate_runtime( } +#ifdef ENABLE_FIX_CWD static int fix_working_directory(const std::wstring &script) { @@ -512,6 +518,7 @@ fix_working_directory(const std::wstring &script) } return hr; } +#endif int @@ -535,7 +542,9 @@ wmain(int argc, wchar_t **argv) const wchar_t *default_cmd; bool use_commands, use_cli_tag, use_shebangs, use_autoinstall; + #ifdef ENABLE_FIX_CWD bool fix_cwd = false; + #endif per_exe_settings(argc, argv, &default_cmd, &use_commands, &use_cli_tag, &use_shebangs, &use_autoinstall); if (use_commands) { @@ -551,10 +560,12 @@ wmain(int argc, wchar_t **argv) // We handle 'exec' in native code, so it won't be in the above list if (!wcscmp(argv[1], L"exec")) { skip_argc += 1; + #ifdef ENABLE_FIX_CWD if (!wcscmp(argv[2], L"--__fix-cwd")) { fix_cwd = true; skip_argc += 1; } + #endif use_cli_tag = argc >= 3; use_shebangs = argc >= 3; default_cmd = NULL; @@ -606,12 +617,14 @@ wmain(int argc, wchar_t **argv) // Theoretically shouldn't matter, but might help reduce memory usage. close_python(); + #ifdef ENABLE_FIX_CWD if (fix_cwd) { err = fix_working_directory(script); if (err) { fprintf(stderr, "[WARN] Failed to fix working directory (0x%08X).\n", err); } } + #endif err = launch(executable.c_str(), args.c_str(), skip_argc, &exitCode);