Phase 2: MSIX 패키지 + IExplorerCommand 셸 익스텐션
C++ 셸 익스텐션 (src/EverythingToJpeg.Shell/) - WRL RuntimeClass 패턴으로 IExplorerCommand 두 핸들러 구현 - Quick(빠른 변환) / Dialog(설정 창) verb를 다른 CLSID로 분리 - Invoke()에서 EverythingToJpeg.exe로 verb + 파일 경로 전달 - VS 2026 빌드 검증, /utf-8 한글 라벨 지원 MSIX 패키징 (packaging/) - Package.appxmanifest: com:SurrogateServer + desktop4:FileExplorerContextMenus - 26개 확장자 × 2 verb 자동 노출 - BuildMsix.ps1: dotnet publish + msbuild + makeappx 일관 파이프라인 - CreateDevCert.ps1 / Install-EverythingToJpeg.ps1: 자체 서명 인증서 워크플로 - GenerateAssets.ps1: placeholder 로고 자동 생성 CI/CD - .github/workflows/release.yml: 태그 푸시 시 미서명 MSIX 자동 빌드 + 첨부 검증 - 50MB MSIX 산출 확인 (packaging/dist/EverythingToJpeg-x64.msix) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8fa4a613d7
commit
f2c8610ff6
19 changed files with 973 additions and 41 deletions
110
src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj
Normal file
110
src/EverythingToJpeg.Shell/EverythingToJpeg.Shell.vcxproj
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{1A2B3C4D-5E6F-7A8B-9C0D-EF1234567890}</ProjectGuid>
|
||||
<RootNamespace>EverythingToJpegShell</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props"
|
||||
Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')"
|
||||
Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<PreprocessorDefinitions>EVERYTHINGTOJPEG_SHELL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<ControlFlowGuard>Guard</ControlFlowGuard>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<CETCompat>true</CETCompat>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="framework.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Source.def" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.260126.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
5
src/EverythingToJpeg.Shell/Source.def
Normal file
5
src/EverythingToJpeg.Shell/Source.def
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
LIBRARY
|
||||
|
||||
EXPORTS
|
||||
DllGetClassObject PRIVATE
|
||||
DllCanUnloadNow PRIVATE
|
||||
188
src/EverythingToJpeg.Shell/dllmain.cpp
Normal file
188
src/EverythingToJpeg.Shell/dllmain.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
// EverythingToJpeg shell extension — IExplorerCommand handlers
|
||||
// Two verbs:
|
||||
// - QuickCommandHandler → "EverythingToJpeg.exe quick "<paths>""
|
||||
// - DialogCommandHandler → "EverythingToJpeg.exe dialog "<paths>""
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#pragma warning(disable : 4324)
|
||||
|
||||
using Microsoft::WRL::ClassicCom;
|
||||
using Microsoft::WRL::ComPtr;
|
||||
using Microsoft::WRL::InhibitRoOriginateError;
|
||||
using Microsoft::WRL::Module;
|
||||
using Microsoft::WRL::ModuleType;
|
||||
using Microsoft::WRL::RuntimeClass;
|
||||
using Microsoft::WRL::RuntimeClassFlags;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const wchar_t* kExeFileName = L"EverythingToJpeg.exe";
|
||||
|
||||
std::wstring QuoteForCommandLineArg(const std::wstring& arg) {
|
||||
const std::wstring quotable_chars(L" \\\"");
|
||||
if (arg.find_first_of(quotable_chars) == std::wstring::npos) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
std::wstring out;
|
||||
out.push_back(L'"');
|
||||
for (size_t i = 0; i < arg.size(); ++i) {
|
||||
if (arg[i] == L'\\') {
|
||||
const size_t start = i;
|
||||
size_t end = start + 1;
|
||||
for (; end < arg.size() && arg[end] == L'\\'; ++end) {}
|
||||
size_t backslash_count = end - start;
|
||||
if (end == arg.size() || arg[end] == L'"') {
|
||||
backslash_count *= 2;
|
||||
}
|
||||
for (size_t j = 0; j < backslash_count; ++j)
|
||||
out.push_back(L'\\');
|
||||
i = end - 1;
|
||||
}
|
||||
else if (arg[i] == L'"') {
|
||||
out.push_back(L'\\');
|
||||
out.push_back(L'"');
|
||||
}
|
||||
else {
|
||||
out.push_back(arg[i]);
|
||||
}
|
||||
}
|
||||
out.push_back(L'"');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveExePath() {
|
||||
std::filesystem::path module_path{
|
||||
wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle()) };
|
||||
module_path = module_path.remove_filename();
|
||||
module_path /= kExeFileName;
|
||||
return module_path;
|
||||
}
|
||||
|
||||
HRESULT LaunchAppWithItems(const wchar_t* verb, IShellItemArray* items) {
|
||||
if (!items) return S_OK;
|
||||
|
||||
DWORD count = 0;
|
||||
RETURN_IF_FAILED(items->GetCount(&count));
|
||||
if (count == 0) return S_OK;
|
||||
|
||||
auto exe_path = ResolveExePath();
|
||||
|
||||
auto command = wil::str_printf<std::wstring>(LR"-("%s" %s)-",
|
||||
exe_path.c_str(), verb);
|
||||
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
ComPtr<IShellItem> item;
|
||||
if (FAILED(items->GetItemAt(i, &item))) continue;
|
||||
|
||||
wil::unique_cotaskmem_string path;
|
||||
if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) continue;
|
||||
|
||||
command = wil::str_printf<std::wstring>(LR"-(%s %s)-",
|
||||
command.c_str(),
|
||||
QuoteForCommandLineArg(path.get()).c_str());
|
||||
}
|
||||
|
||||
wil::unique_process_information process_info;
|
||||
STARTUPINFOW startup_info = { sizeof(startup_info) };
|
||||
RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(
|
||||
nullptr,
|
||||
command.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&startup_info,
|
||||
&process_info));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
template <typename Derived>
|
||||
class CommandHandlerBase : public RuntimeClass<
|
||||
RuntimeClassFlags<ClassicCom | InhibitRoOriginateError>,
|
||||
IExplorerCommand>
|
||||
{
|
||||
public:
|
||||
IFACEMETHODIMP GetTitle(IShellItemArray*, PWSTR* name) override {
|
||||
return SHStrDupW(Derived::Title(), name);
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetIcon(IShellItemArray*, PWSTR* icon) override {
|
||||
auto exe = ResolveExePath();
|
||||
return SHStrDupW(exe.c_str(), icon);
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetToolTip(IShellItemArray*, PWSTR* infoTip) override {
|
||||
*infoTip = nullptr;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetCanonicalName(GUID* guidCommandName) override {
|
||||
*guidCommandName = GUID_NULL;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetState(IShellItemArray*, BOOL, EXPCMDSTATE* cmdState) override {
|
||||
*cmdState = ECS_ENABLED;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override {
|
||||
*flags = ECF_DEFAULT;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** enumCommands) override {
|
||||
*enumCommands = nullptr;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP Invoke(IShellItemArray* items, IBindCtx*) override {
|
||||
return LaunchAppWithItems(Derived::Verb(), items);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class __declspec(uuid("801B2DD3-632C-4731-9510-AEAE09345264"))
|
||||
QuickCommandHandler final
|
||||
: public CommandHandlerBase<QuickCommandHandler>
|
||||
{
|
||||
public:
|
||||
static constexpr const wchar_t* Title() { return L"JPEG로 빠른 변환"; }
|
||||
static constexpr const wchar_t* Verb() { return L"quick"; }
|
||||
};
|
||||
|
||||
class __declspec(uuid("CEBA1DB7-9175-4DF6-A362-490DEA49B598"))
|
||||
DialogCommandHandler final
|
||||
: public CommandHandlerBase<DialogCommandHandler>
|
||||
{
|
||||
public:
|
||||
static constexpr const wchar_t* Title() { return L"JPEG로 변환…"; }
|
||||
static constexpr const wchar_t* Verb() { return L"dialog"; }
|
||||
};
|
||||
|
||||
CoCreatableClass(QuickCommandHandler)
|
||||
CoCreatableClass(DialogCommandHandler)
|
||||
CoCreatableClassWrlCreatorMapInclude(QuickCommandHandler)
|
||||
CoCreatableClassWrlCreatorMapInclude(DialogCommandHandler)
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
_Check_return_
|
||||
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
|
||||
if (ppv == nullptr) return E_POINTER;
|
||||
*ppv = nullptr;
|
||||
return Module<ModuleType::InProc>::GetModule().GetClassObject(rclsid, riid, ppv);
|
||||
}
|
||||
|
||||
__control_entrypoint(DllExport)
|
||||
STDAPI DllCanUnloadNow(void) {
|
||||
return Module<ModuleType::InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
|
||||
}
|
||||
4
src/EverythingToJpeg.Shell/framework.h
Normal file
4
src/EverythingToJpeg.Shell/framework.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
1
src/EverythingToJpeg.Shell/pch.cpp
Normal file
1
src/EverythingToJpeg.Shell/pch.cpp
Normal file
|
|
@ -0,0 +1 @@
|
|||
#include "pch.h"
|
||||
24
src/EverythingToJpeg.Shell/pch.h
Normal file
24
src/EverythingToJpeg.Shell/pch.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
#include "framework.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <shlobj_core.h>
|
||||
#include <shlwapi.h>
|
||||
#pragma comment(lib, "shlwapi.lib")
|
||||
|
||||
#include <wrl/client.h>
|
||||
#include <wrl/implements.h>
|
||||
#include <wrl/module.h>
|
||||
#pragma comment(lib, "runtimeobject.lib")
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 28182)
|
||||
#include <wil/stl.h>
|
||||
#include <wil/win32_helpers.h>
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue