From 36be4ce4ecde654591d27553553673346df94705 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 6 May 2025 12:37:19 +0100 Subject: [PATCH 1/3] Enables cross-platform DLL build of the shell extension (for MSI installs) --- _msbuild.py | 45 +++++++++++++++++++------------- make-msix.py | 15 +++++++++++ src/pymanager/msi.wxs | 23 ++++++++--------- src/pyshellext/pyshellext.def | 3 +++ src/pyshellext/shellext.cpp | 48 ++++++++++++++++++++++++++++++++--- 5 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 src/pyshellext/pyshellext.def diff --git a/_msbuild.py b/_msbuild.py index 2d183ab..1e65d7d 100644 --- a/_msbuild.py +++ b/_msbuild.py @@ -148,6 +148,31 @@ def launcher_exe(name, platform, windowed=False): ) +def pyshellext(ext='.exe', **props): + link_opts = ItemDefinition( + 'Link', + AdditionalDependencies=Prepend('RuntimeObject.lib;'), + ) + if ext != '.exe': + link_opts.options['ModuleDefinitionFile'] = '$(SourceRootDir)src\\pyshellext\\pyshellext.def' + + + return CProject(f"pyshellext{ext.rpartition('.')[0]}", + VersionInfo( + FileDescription='Python shell extension', + OriginalFilename=f'pyshellext{ext}', + ), + ItemDefinition('ClCompile', LanguageStandard='stdcpp20'), + link_opts, + Manifest('default.manifest'), + CSourceFile('shellext.cpp'), + ResourceFile('pyshellext.rc'), + source='src/pyshellext', + StaticLibcppLinkage=True, + **props, + ) + + PACKAGE = Package('python-manager', PyprojectTomlFile('pyproject.toml'), # MSIX manifest @@ -206,23 +231,9 @@ def launcher_exe(name, platform, windowed=False): main_exe("python3"), mainw_exe("pythonw3"), - CProject("pyshellext", - VersionInfo( - FileDescription="Python shell extension", - OriginalFilename="pyshellext.exe", - ), - Property('StaticLibcppLinkage', 'true'), - ItemDefinition('ClCompile', LanguageStandard='stdcpp20'), - ItemDefinition('Link', - AdditionalDependencies=Prepend("RuntimeObject.lib;"), - SubSystem='WINDOWS', - ), - Manifest('default.manifest'), - CSourceFile('shellext.cpp'), - ResourceFile('pyshellext.rc'), - source='src/pyshellext', - ConfigurationType='Application', - ), + pyshellext(".exe", ConfigurationType="Application"), + pyshellext("-64.dll", Platform="x64"), + pyshellext("-arm64.dll", Platform="ARM64"), ) diff --git a/make-msix.py b/make-msix.py index 84f3e70..cc07fdc 100644 --- a/make-msix.py +++ b/make-msix.py @@ -53,10 +53,19 @@ "/mf", "appx"]) # Clean up non-shipping files from LAYOUT +preserved = [ + *LAYOUT.glob("pyshellext*.dll"), +] + +for f in preserved: + print("Preserving", f, "as", TEMP / f.name) + copyfile(f, TEMP / f.name) + unlink( *LAYOUT.rglob("*.pdb"), *LAYOUT.rglob("*.pyc"), *LAYOUT.rglob("__pycache__"), + *preserved, ) # Package into DIST @@ -122,3 +131,9 @@ def patch_appx(source): with zipfile.ZipFile(DIST_MSIXUPLOAD, "w") as zf: zf.write(DIST_STORE_MSIX, arcname=DIST_STORE_MSIX.name) zf.write(DIST_APPXSYM, arcname=DIST_APPXSYM.name) + + +for f in preserved: + print("Restoring", f, "from", TEMP / f.name) + copyfile(TEMP / f.name, f) + unlink(TEMP / f.name) diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index a9a33c1..6e57c75 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -27,6 +27,17 @@ + @@ -44,18 +55,6 @@ - - diff --git a/src/pyshellext/pyshellext.def b/src/pyshellext/pyshellext.def new file mode 100644 index 0000000..751bc6c --- /dev/null +++ b/src/pyshellext/pyshellext.def @@ -0,0 +1,3 @@ +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE diff --git a/src/pyshellext/shellext.cpp b/src/pyshellext/shellext.cpp index 991e816..c747337 100644 --- a/src/pyshellext/shellext.cpp +++ b/src/pyshellext/shellext.cpp @@ -316,11 +316,12 @@ class DECLSPEC_UUID(CLSID_COMMAND_ENUMERATOR) CommandEnumerator class DECLSPEC_UUID(CLSID_IDLE_COMMAND) IdleCommand - : public RuntimeClass, IExplorerCommand> + : public RuntimeClass, IExplorerCommand, IObjectWithSite> { std::vector idles; std::wstring iconPath; std::wstring title; + ComPtr _site; public: IdleCommand() : title(L"Edit in &IDLE") { @@ -432,12 +433,28 @@ class DECLSPEC_UUID(CLSID_IDLE_COMMAND) IdleCommand ).Detach(); return S_OK; } + + IFACEMETHODIMP GetSite(REFIID riid, void **ppvSite) + { + if (_site) { + return _site->QueryInterface(riid, ppvSite); + } + *ppvSite = NULL; + return E_FAIL; + } + + IFACEMETHODIMP SetSite(IUnknown *pSite) + { + _site = pSite; + return S_OK; + } }; CoCreatableClass(IdleCommand); #ifdef PYSHELLEXT_TEST + IExplorerCommand *MakeLaunchCommand(std::wstring title, std::wstring exe, std::wstring idle) { IdleData data = { .title = title, .exe = exe, .idle = idle }; @@ -449,10 +466,34 @@ IExplorerCommand *MakeIdleCommand(HKEY hive, LPCWSTR root) { return Make(hive, root).Detach(); } -#endif +#elif defined(_WINDLL) + +#pragma comment(linker, "/export:DllGetClassObject") +#pragma comment(linker, "/export:DllCanUnloadNow") + +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, _COM_Outptr_ void** ppv) +{ + return Module::GetModule().GetClassObject(rclsid, riid, ppv); +} + + +STDAPI DllCanUnloadNow() +{ + return Module::GetModule().Terminate() ? S_OK : S_FALSE; +} + +STDAPI_(BOOL) DllMain(_In_opt_ HINSTANCE hinst, DWORD reason, _In_opt_ void*) +{ + if (reason == DLL_PROCESS_ATTACH) { + hModule = hinst; + DisableThreadLibraryCalls(hinst); + } + return TRUE; +} + +#else -#ifndef PYSHELLEXT_TEST class OutOfProcModule : public Module { }; @@ -475,4 +516,5 @@ int WINAPI wWinMain( CoUninitialize(); return 0; } + #endif From 0b6e085a67f428f068a9913c764b39029b37c6cc Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 8 May 2025 15:33:56 +0100 Subject: [PATCH 2/3] Add MSI code for shell extension installation --- src/pymanager/msi.wxs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/pymanager/msi.wxs b/src/pymanager/msi.wxs index 6e57c75..320dfad 100644 --- a/src/pymanager/msi.wxs +++ b/src/pymanager/msi.wxs @@ -27,20 +27,12 @@ - + + + @@ -114,5 +106,21 @@ + + + + + + + + + + + + + + From c4745a460512fd3efcdfea94020dd2cad6e56fdf Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 8 May 2025 19:57:04 +0100 Subject: [PATCH 3/3] Minor code cleanup --- src/pyshellext/shellext.cpp | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/pyshellext/shellext.cpp b/src/pyshellext/shellext.cpp index c747337..974bf92 100644 --- a/src/pyshellext/shellext.cpp +++ b/src/pyshellext/shellext.cpp @@ -167,7 +167,7 @@ HRESULT ReadAllIdleInstalls(std::vector &idles, HKEY hive, LPCWSTR roo } class DECLSPEC_UUID(CLSID_LAUNCH_COMMAND) LaunchCommand - : public RuntimeClass, IExplorerCommand> + : public RuntimeClass, IExplorerCommand, IObjectWithSite> { std::wstring title; std::wstring exe; @@ -269,6 +269,26 @@ class DECLSPEC_UUID(CLSID_LAUNCH_COMMAND) LaunchCommand *ppEnum = NULL; return E_NOTIMPL; } + + // IObjectWithSite +private: + ComPtr _site; + +public: + IFACEMETHODIMP GetSite(REFIID riid, void **ppvSite) + { + if (_site) { + return _site->QueryInterface(riid, ppvSite); + } + *ppvSite = NULL; + return E_FAIL; + } + + IFACEMETHODIMP SetSite(IUnknown *pSite) + { + _site = pSite; + return S_OK; + } }; @@ -321,7 +341,6 @@ class DECLSPEC_UUID(CLSID_IDLE_COMMAND) IdleCommand std::vector idles; std::wstring iconPath; std::wstring title; - ComPtr _site; public: IdleCommand() : title(L"Edit in &IDLE") { @@ -434,6 +453,11 @@ class DECLSPEC_UUID(CLSID_IDLE_COMMAND) IdleCommand return S_OK; } + // IObjectWithSite +private: + ComPtr _site; + +public: IFACEMETHODIMP GetSite(REFIID riid, void **ppvSite) { if (_site) { @@ -469,9 +493,6 @@ IExplorerCommand *MakeIdleCommand(HKEY hive, LPCWSTR root) #elif defined(_WINDLL) -#pragma comment(linker, "/export:DllGetClassObject") -#pragma comment(linker, "/export:DllCanUnloadNow") - STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, _COM_Outptr_ void** ppv) { return Module::GetModule().GetClassObject(rclsid, riid, ppv);