diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 79ffafa0f1..0788832d5c 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -14,6 +14,7 @@ on: - 'resources/**' - ".github/workflows/build_*.yml" - 'scripts/flatpak/**' + - 'scripts/msix/**' pull_request: branches: @@ -30,6 +31,7 @@ on: - 'build_release_vs2022.bat' - 'build_release_macos.sh' - 'scripts/flatpak/**' + - 'scripts/msix/**' schedule: diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 7ccb52b0e6..1e8588a373 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -371,6 +371,25 @@ jobs: asset_content_type: application/x-msdownload max_releases: 1 + - name: Build MSIX Store package Win + if: runner.os == 'Windows' && !vars.SELF_HOSTED + working-directory: ${{ github.workspace }} + shell: pwsh + run: | + ./scripts/msix/build_msix.ps1 ` + -InstallDir "${{ github.workspace }}/build/OrcaSlicer" ` + -OutputPath "${{ github.workspace }}/build/OrcaSlicer_Windows_MSIX_${{ env.ver }}.msix" ` + -IdentityName "${{ vars.ORCA_MSIX_IDENTITY_NAME || 'OrcaSlicer.OrcaSlicer' }}" ` + -Publisher "${{ vars.ORCA_MSIX_PUBLISHER || 'CN=38F7EA55-C73B-4072-B3B2-C8E0EA15BB82' }}" ` + -PublisherDisplayName "${{ vars.ORCA_MSIX_PUBLISHER_DISPLAY_NAME || 'OrcaSlicer' }}" + + - name: Upload artifacts Win MSIX + if: runner.os == 'Windows' && !vars.SELF_HOSTED + uses: actions/upload-artifact@v7 + with: + name: OrcaSlicer_Windows_MSIX_${{ env.ver }} + path: ${{ github.workspace }}/build/OrcaSlicer_Windows_MSIX_${{ env.ver }}.msix + # Ubuntu - name: Apt-Install Dependencies if: runner.os == 'Linux' && !vars.SELF_HOSTED diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 7c2999e872..98514d99db 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -73,8 +73,9 @@ jobs: - name: Download release artifacts from build run run: | + # Windows_V* (not Windows_*) keeps the MSIX Store artifact out: it goes to Partner Center, not GitHub releases. gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" --dir artifacts \ - -p 'OrcaSlicer_Windows_*' \ + -p 'OrcaSlicer_Windows_V*' \ -p 'OrcaSlicer_Mac_universal_*' \ -p 'OrcaSlicer_Linux_ubuntu_*' \ -p 'OrcaSlicer-Linux-flatpak_*' \ diff --git a/scripts/msix/AppxManifest.xml b/scripts/msix/AppxManifest.xml new file mode 100644 index 0000000000..56477bf80a --- /dev/null +++ b/scripts/msix/AppxManifest.xml @@ -0,0 +1,75 @@ + + + + + + + OrcaSlicer + @MSIX_PUBLISHER_DISPLAY_NAME@ + Assets\StoreLogo.png + + disabled + + + $(KnownFolder:RoamingAppData)\OrcaSlicer + + + + + + + + + + + + + + + + + + + + .3mf + .stl + .step + .stp + .gcode + .drc + + + Orca.Slicer.1 + + + + + + + + + + + + + + + diff --git a/scripts/msix/assets/Square150x150Logo.png b/scripts/msix/assets/Square150x150Logo.png new file mode 100644 index 0000000000..546b5c57f9 Binary files /dev/null and b/scripts/msix/assets/Square150x150Logo.png differ diff --git a/scripts/msix/assets/Square44x44Logo.png b/scripts/msix/assets/Square44x44Logo.png new file mode 100644 index 0000000000..248a2050d2 Binary files /dev/null and b/scripts/msix/assets/Square44x44Logo.png differ diff --git a/scripts/msix/assets/Square44x44Logo.targetsize-44_altform-unplated.png b/scripts/msix/assets/Square44x44Logo.targetsize-44_altform-unplated.png new file mode 100644 index 0000000000..248a2050d2 Binary files /dev/null and b/scripts/msix/assets/Square44x44Logo.targetsize-44_altform-unplated.png differ diff --git a/scripts/msix/assets/StoreLogo.png b/scripts/msix/assets/StoreLogo.png new file mode 100644 index 0000000000..eb5a9b0c75 Binary files /dev/null and b/scripts/msix/assets/StoreLogo.png differ diff --git a/scripts/msix/build_msix.ps1 b/scripts/msix/build_msix.ps1 new file mode 100644 index 0000000000..55e9fc156f --- /dev/null +++ b/scripts/msix/build_msix.ps1 @@ -0,0 +1,67 @@ +<# +Builds the unsigned MSIX Store package from an existing install tree. +The package is intentionally NOT signed: the Microsoft Store strips and +re-signs uploads with Microsoft's certificate. For local installs use +Developer Mode loose-layout registration instead: + ./scripts/msix/build_msix.ps1 -StageOnly + Add-AppxPackage -Register \AppxManifest.xml +Requires the Windows SDK (makeappx.exe) unless -StageOnly is used. +#> +param( + [string]$InstallDir = "build/OrcaSlicer", + [string]$OutputPath = "build/OrcaSlicer_Windows_MSIX.msix", + [string]$StagingDir = "", + [switch]$StageOnly, + [string]$IdentityName = "OrcaSlicer.OrcaSlicer", + [string]$Publisher = "CN=38F7EA55-C73B-4072-B3B2-C8E0EA15BB82", + [string]$PublisherDisplayName = "OrcaSlicer" +) +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + +# MSIX version = MAJOR.MINOR.PATCH.0 from the SoftFever_VERSION semver triplet +# (Store requires the revision field to be 0). +$versionContent = Get-Content (Join-Path $repoRoot 'version.inc') -Raw +if ($versionContent -notmatch 'set\(SoftFever_VERSION "(\d+)\.(\d+)\.(\d+)') { + throw "Could not parse SoftFever_VERSION from version.inc" +} +$msixVersion = "$($Matches[1]).$($Matches[2]).$($Matches[3]).0" +Write-Output "MSIX version: $msixVersion" + +if (-not (Test-Path (Join-Path $InstallDir 'orca-slicer.exe'))) { + throw "orca-slicer.exe not found in '$InstallDir' - build the install tree first" +} + +if ([string]::IsNullOrEmpty($StagingDir)) { + $StagingDir = Join-Path ([System.IO.Path]::GetTempPath()) 'orca-msix-staging' +} +if (Test-Path $StagingDir) { Remove-Item $StagingDir -Recurse -Force } +New-Item -ItemType Directory -Force $StagingDir | Out-Null + +Copy-Item -Path (Join-Path $InstallDir '*') -Destination $StagingDir -Recurse +Copy-Item -Path (Join-Path $PSScriptRoot 'assets') -Destination (Join-Path $StagingDir 'Assets') -Recurse + +$manifest = Get-Content (Join-Path $PSScriptRoot 'AppxManifest.xml') -Raw +$manifest = $manifest.Replace('@MSIX_VERSION@', $msixVersion) +$manifest = $manifest.Replace('@MSIX_IDENTITY_NAME@', $IdentityName) +$manifest = $manifest.Replace('@MSIX_PUBLISHER@', $Publisher) +$manifest = $manifest.Replace('@MSIX_PUBLISHER_DISPLAY_NAME@', $PublisherDisplayName) +Set-Content -Path (Join-Path $StagingDir 'AppxManifest.xml') -Value $manifest -Encoding utf8 + +if ($StageOnly) { + Write-Output "Staged loose layout at: $StagingDir" + return +} + +$makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\x64\makeappx.exe" -ErrorAction SilentlyContinue | + Sort-Object { [version]$_.Directory.Parent.Name } -Descending | + Select-Object -First 1 -ExpandProperty FullName +if (-not $makeappx) { + throw "makeappx.exe not found under '${env:ProgramFiles(x86)}\Windows Kits\10\bin' - install the Windows SDK" +} +Write-Output "Using makeappx: $makeappx" + +& $makeappx pack /d $StagingDir /p $OutputPath /o +if ($LASTEXITCODE -ne 0) { throw "makeappx pack failed with exit code $LASTEXITCODE" } +Write-Output "Packed: $OutputPath" diff --git a/scripts/msix/generate_assets.ps1 b/scripts/msix/generate_assets.ps1 new file mode 100644 index 0000000000..52071cb7be --- /dev/null +++ b/scripts/msix/generate_assets.ps1 @@ -0,0 +1,56 @@ +# Generates the MSIX package logo assets from the master vector logo +# (resources\images\OrcaSlicer_gradient_circle.svg). Each PNG is rendered from +# the SVG at its exact target size (true per-size vector rasterization, not +# downscaled from one bitmap), preserving alpha transparency in the corners +# outside the circle (the manifest uses BackgroundColor="transparent"). +# +# Run once locally on Windows (re-run only if the logo changes), then commit +# the PNGs in assets/. CI never runs this script. +# +# Prerequisite: Python 3 with the resvg-py package (pip install resvg-py). +# It bundles the resvg SVG renderer, needed because the master SVG uses +# gradients with alpha-fade stops that System.Drawing cannot rasterize. +param( + [string]$Python = 'python' +) +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$source = Join-Path $repoRoot 'resources\images\OrcaSlicer_gradient_circle.svg' +$outDir = Join-Path $PSScriptRoot 'assets' +New-Item -ItemType Directory -Force $outDir | Out-Null + +$sizes = [ordered]@{ + 'Square150x150Logo.png' = 150 + 'Square44x44Logo.png' = 44 + 'Square44x44Logo.targetsize-44_altform-unplated.png' = 44 + 'StoreLogo.png' = 50 +} + +$py = @' +import sys +from pathlib import Path + +import resvg_py + +svg, out_dir = sys.argv[1], Path(sys.argv[2]) +for spec in sys.argv[3:]: + name, px = spec.rsplit('=', 1) + px = int(px) + data = resvg_py.svg_to_bytes(svg_path=svg, width=px, height=px) + (out_dir / name).write_bytes(bytes(data)) + print(f'Wrote {name} ({px}x{px})') +'@ + +$renderScript = Join-Path $env:TEMP 'orca_msix_render.py' +Set-Content -Path $renderScript -Value $py -Encoding utf8 +try { + $specs = foreach ($name in $sizes.Keys) { "$name=$($sizes[$name])" } + & $Python $renderScript $source $outDir @specs + if ($LASTEXITCODE -ne 0) { + throw 'resvg render failed. Is resvg-py installed? (pip install resvg-py)' + } +} +finally { + Remove-Item $renderScript -ErrorAction SilentlyContinue +} diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 53c987fabb..4b35274867 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2859,7 +2859,11 @@ bool GUI_App::on_init_inner() switch (dialog.ShowModal()) { case wxID_YES: - wxLaunchDefaultBrowser(version_info.url); + // Store builds get updates from the Microsoft Store, not the GitHub release page. + if (is_running_in_msix()) + open_ms_store_product_page(); + else + wxLaunchDefaultBrowser(version_info.url); break; case wxID_NO: break; @@ -9112,6 +9116,10 @@ static bool del_win_registry(HKEY hkeyHive, const wchar_t *pszVar, const wchar_t void GUI_App::associate_files(std::wstring extend) { #ifdef WIN32 + // MSIX: shell integration is declared in the package manifest; registry + // writes from a packaged process are virtualized and invisible to the shell. + if (is_running_in_msix()) + return; wchar_t app_path[MAX_PATH]; ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); @@ -9137,6 +9145,8 @@ void GUI_App::associate_files(std::wstring extend) void GUI_App::disassociate_files(std::wstring extend) { #ifdef WIN32 + if (is_running_in_msix()) + return; wchar_t app_path[MAX_PATH]; ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); @@ -9188,6 +9198,8 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b void GUI_App::associate_url(std::wstring url_prefix) { #ifdef WIN32 + if (is_running_in_msix()) + return; boost::filesystem::path binary_path(boost::filesystem::canonical(boost::dll::program_location())); wxString wbinary = from_path(binary_path); BOOST_LOG_TRIVIAL(info) << "Downloader registration: Path of binary: " << wbinary.ToUTF8().data(); @@ -9213,6 +9225,8 @@ void GUI_App::associate_url(std::wstring url_prefix) void GUI_App::disassociate_url(std::wstring url_prefix) { #ifdef WIN32 + if (is_running_in_msix()) + return; wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command"); if (!key_full.Exists()) { return; diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index a16d9916a6..697eefea0b 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "libslic3r/Config.hpp" @@ -171,6 +172,42 @@ template typename F::FN winapi_get_function(const wchar_t *dll, const c } #endif +bool is_running_in_msix() +{ +#ifdef _WIN32 + // The package identity APIs are Win8+ - resolved dynamically so the exe still loads on Win7 + // (same treatment as the DPI APIs below). Null-buffer probe: returns ERROR_INSUFFICIENT_BUFFER + // when packaged, APPMODEL_ERROR_NO_PACKAGE when running unpackaged. + struct GetCurrentPackageFullName_t { typedef LONG (WINAPI *FN)(UINT32 *length, PWSTR full_name); }; + static const bool packaged = []() { + auto fn = winapi_get_function(L"Kernel32.dll", "GetCurrentPackageFullName"); + UINT32 length = 0; + return fn != nullptr && fn(&length, nullptr) != APPMODEL_ERROR_NO_PACKAGE; + }(); + return packaged; +#else + return false; +#endif +} + +void open_ms_store_product_page() +{ +#ifdef _WIN32 + struct GetCurrentPackageFamilyName_t { typedef LONG (WINAPI *FN)(UINT32 *length, PWSTR family_name); }; + static auto fn = winapi_get_function(L"Kernel32.dll", "GetCurrentPackageFamilyName"); + if (fn == nullptr) + return; + UINT32 length = 0; + if (fn(&length, nullptr) != ERROR_INSUFFICIENT_BUFFER) + return; + std::wstring family_name(length, L'\0'); + if (fn(&length, family_name.data()) != ERROR_SUCCESS) + return; + family_name.resize(length > 0 ? length - 1 : 0); // drop the terminating null + wxLaunchDefaultBrowser(wxString(L"ms-windows-store://pdp/?PFN=") + family_name.c_str()); +#endif +} + // If called with nullptr, a DPI for the primary monitor is returned. int get_dpi_for_window(const wxWindow *window) { diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index e50c9254a9..d6767d3310 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -67,6 +67,10 @@ wxDECLARE_EVENT(EVT_VOLUME_DETACHED, VolumeDetachedEvent); wxTopLevelWindow* find_toplevel_parent(wxWindow *window); wxString format_nozzle_diameter(float diameter); +// True when running inside an MSIX package (Microsoft Store build); always false on non-Windows. +bool is_running_in_msix(); +// Opens the Microsoft Store product page for the current package. No-op when not packaged. +void open_ms_store_product_page(); void on_window_geometry(wxTopLevelWindow *tlw, std::function callback); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 1e6b1f6558..ef3d8fcee5 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1846,6 +1846,26 @@ void PreferencesDialog::create_items() //// ASSOCIATE TAB ///////////////////////////////////// #ifdef _WIN32 + // MSIX: associations are declared in the package manifest and defaults are + // managed by Windows Settings; the runtime registry toggles below cannot work. + // Show a minimal page that sends the user to Windows' Default Apps settings instead. + if (is_running_in_msix()) { + m_pref_tabs->AppendItem(_L("Associate")); + f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); + g_sizer = f_sizers.back(); + g_sizer->AddGrowableCol(0, 1); + + g_sizer->Add(create_item_title(_L("Associate files to OrcaSlicer")), 1, wxEXPAND); + + auto item_open_default_apps = create_item_button( + _L("File associations for the Microsoft Store version are managed by Windows Settings."), + _L("Open Windows Default Apps Settings"), "", "", + []() { wxLaunchDefaultBrowser("ms-settings:defaultapps"); }); + g_sizer->Add(item_open_default_apps); + + g_sizer->AddSpacer(FromDIP(10)); + sizer_page->Add(g_sizer, 0, wxEXPAND); + } else { m_pref_tabs->AppendItem(_L("Associate")); f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0)); g_sizer = f_sizers.back(); @@ -1880,6 +1900,7 @@ void PreferencesDialog::create_items() g_sizer->AddSpacer(FromDIP(10)); sizer_page->Add(g_sizer, 0, wxEXPAND); + } #endif // _WIN32 ////////////////////////// diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 3dd87e8626..84546a941d 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -5,6 +5,7 @@ #include "libslic3r/Thread.hpp" #include "GUI.hpp" #include "GUI_App.hpp" +#include "GUI_Utils.hpp" #include "GUI_Preview.hpp" #include "MainFrame.hpp" #include "format.hpp" @@ -252,7 +253,9 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent) m_text_up_info = new Label(this, Label::Head_14, wxEmptyString, LB_AUTO_WRAP); m_text_up_info->SetForegroundColour(wxColour(0x26, 0x2E, 0x30)); - auto github_link = new HyperLink(this, _L("Check on Github"), "", LB_AUTO_WRAP); + // Store builds get updates from the Microsoft Store: wxID_YES opens the Store + // product page there (see the EVT_SLIC3R_VERSION_ONLINE handler) instead of GitHub. + auto github_link = new HyperLink(this, is_running_in_msix() ? _L("Check on Microsoft Store") : _L("Check on Github"), "", LB_AUTO_WRAP); github_link->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &e) { EndModal(wxID_YES); }); @@ -302,7 +305,7 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent) auto sizer_button = new wxBoxSizer(wxHORIZONTAL); - m_button_download = new Button(this, _L("Download")); + m_button_download = new Button(this, is_running_in_msix() ? _L("Open Microsoft Store") : _L("Download")); m_button_download->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); m_button_download->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &e) { @@ -479,7 +482,10 @@ void UpdateVersionDialog::update_version_info(wxString release_note, wxString ve // else { //m_simplebook_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); m_simplebook_release_note->SetSelection(1); - m_text_up_info->SetLabel(wxString::Format(_L("Click to download new version in default browser: %s"), version)); + if (is_running_in_msix()) + m_text_up_info->SetLabel(wxString::Format(_L("New version available: %s. Please update OrcaSlicer from the Microsoft Store."), version)); + else + m_text_up_info->SetLabel(wxString::Format(_L("Click to download new version in default browser: %s"), version)); auto data_buf_in = release_note.utf8_str(); auto bg_color = StateColor::darkModeColorFor(wxColour("#FFFFFF")).GetAsString(); auto fg_color = StateColor::darkModeColorFor(wxColour("#262E30")).GetAsString();