mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-18 21:20:57 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f307785cad | ||
|
|
6bc835ea30 |
No files matched your search
@@ -21,8 +21,6 @@ on:
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'signpath/**'
|
||||
- 'scripts/*.ps1'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
@@ -40,13 +38,11 @@ on:
|
||||
- 'version.inc'
|
||||
- ".github/workflows/build_*.yml"
|
||||
- ".github/workflows/unit_tests*.yml"
|
||||
- 'signpath/**'
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/*.ps1'
|
||||
- 'scripts/flatpak/**'
|
||||
- 'scripts/msix/**'
|
||||
- 'tests/**'
|
||||
|
||||
@@ -424,10 +424,6 @@ jobs:
|
||||
"BUILD_DIR=build" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
"ARCH_SUFFIX=_x64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
}
|
||||
# Release branches ship binaries and installers signed through SignPath with the
|
||||
# release certificate. Every signing request waits for a manual approval in
|
||||
# SignPath, so main and pull request builds stay unsigned.
|
||||
"SIGN_WINDOWS=${{ github.repository == 'OrcaSlicer/OrcaSlicer' && startsWith(github.ref, 'refs/heads/release/') && !vars.SELF_HOSTED }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
|
||||
- name: setup MSVC
|
||||
if: runner.os == 'Windows'
|
||||
@@ -508,129 +504,19 @@ jobs:
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
# Signs OrcaSlicer's own binaries and every bundled executable, DLL and Python
|
||||
# extension module that ships without a signature; files their vendor signed, such
|
||||
# as the Microsoft runtime, keep that signature. See signpath/README.md.
|
||||
- name: Stage Windows binaries for SignPath
|
||||
id: stage-windows-binaries
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = "${{ env.ver }}" -replace '^V', ''
|
||||
./scripts/stage-for-signpath.ps1 -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries" -ProductVersion $version
|
||||
"version=$version" | Out-File -Append -FilePath $env:GITHUB_OUTPUT -Encoding utf8
|
||||
|
||||
- name: Upload unsigned Windows binaries for SignPath
|
||||
id: upload-windows-binaries
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signpath_windows_binaries${{ env.ARCH_SUFFIX }}_unsigned
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
# Waits until the request is approved in SignPath.
|
||||
- name: Sign Windows binaries with SignPath
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: OrcaSlicer
|
||||
signing-policy-slug: release-signing
|
||||
artifact-configuration-slug: windows-binaries-v1
|
||||
github-artifact-id: ${{ steps.upload-windows-binaries.outputs.artifact-id }}
|
||||
parameters: |
|
||||
version: "${{ steps.stage-windows-binaries.outputs.version }}"
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 10800
|
||||
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries
|
||||
|
||||
- name: Verify Windows binary signatures
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries'
|
||||
|
||||
- name: Replace Windows binaries with signed output
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/restore-from-signpath.ps1 -SignedDirectory "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries" -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer"
|
||||
|
||||
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
|
||||
# x86 emulation, packaging the native arm64 payload from build-arm64.
|
||||
# The installer packs the installed OrcaSlicer/ tree, which the portable zip and the
|
||||
# MSIX are made from too, instead of re-running the install rules: those take
|
||||
# CPython and uv from the dependency tree and the download cache, which keep the
|
||||
# unsigned copies.
|
||||
- name: Create installer Win
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
|
||||
run: |
|
||||
$bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/')
|
||||
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;."
|
||||
|
||||
# cpack stages what the installer packs under _CPack_Packages/<system>/NSIS/<package>;
|
||||
# every executable and DLL there must carry a valid signature, ours or its vendor's.
|
||||
- name: Verify every binary the installer packs is signed
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$payload = Get-Item "${{ github.workspace }}/${{ env.BUILD_DIR }}/_CPack_Packages/*/NSIS/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}"
|
||||
./scripts/verify-authenticode.ps1 -ArtifactDirectory $payload.FullName
|
||||
|
||||
- name: Stage Windows installer for SignPath
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/stage-for-signpath.ps1 -Path "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer" -ProductVersion "${{ steps.stage-windows-binaries.outputs.version }}"
|
||||
|
||||
- name: Upload unsigned Windows installer for SignPath
|
||||
id: upload-windows-installer
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: signpath_windows_installer${{ env.ARCH_SUFFIX }}_unsigned
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Sign Windows installer with SignPath
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: OrcaSlicer
|
||||
signing-policy-slug: release-signing
|
||||
artifact-configuration-slug: windows-installer-v1
|
||||
github-artifact-id: ${{ steps.upload-windows-installer.outputs.artifact-id }}
|
||||
parameters: |
|
||||
version: "${{ steps.stage-windows-binaries.outputs.version }}"
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer
|
||||
|
||||
- name: Verify Windows installer signature
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer' -Files 'OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe'
|
||||
|
||||
- name: Replace Windows installer with signed output
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: Copy-Item -LiteralPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}" -Force
|
||||
cpack -G NSIS
|
||||
|
||||
- name: Pack app
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$zipPath = "OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip"
|
||||
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
|
||||
& "C:/Program Files/7-Zip/7z.exe" a -tzip $zipPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
shell: cmd
|
||||
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer'
|
||||
|
||||
- name: Pack PDB
|
||||
if: runner.os == 'Windows' && inputs.arch != 'arm64' && !vars.SELF_HOSTED
|
||||
@@ -644,7 +530,6 @@ jobs:
|
||||
with:
|
||||
name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable
|
||||
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts Win installer
|
||||
if: runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
|
||||
@@ -1355,18 +1355,6 @@ set (CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
|
||||
set(CPACK_PACKAGE_EXECUTABLES "orca-slicer;OrcaSlicer")
|
||||
set(CPACK_CREATE_DESKTOP_LINKS "orca-slicer")
|
||||
set (CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE.txt) # must also include in install command
|
||||
# Version information of the installer, the same as the binaries it installs carry
|
||||
# (src/dev-utils/platform/msw/OrcaSlicer-version.rc.in): SignPath only signs it when
|
||||
# ProductName and ProductVersion match signpath/windows-installer-v1.xml. The NSIS strings
|
||||
# are single-quoted because CPack writes this value into CPackConfig.cmake unescaped.
|
||||
string (CONCAT CPACK_NSIS_DEFINES
|
||||
"VIProductVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n"
|
||||
"VIFileVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n"
|
||||
"VIAddVersionKey /LANG=1033 'CompanyName' 'SoftFever'\n"
|
||||
"VIAddVersionKey /LANG=1033 'FileDescription' '${SLIC3R_APP_NAME} Installer'\n"
|
||||
"VIAddVersionKey /LANG=1033 'FileVersion' '${SoftFever_VERSION}'\n"
|
||||
"VIAddVersionKey /LANG=1033 'ProductName' '${SLIC3R_APP_NAME}'\n"
|
||||
"VIAddVersionKey /LANG=1033 'ProductVersion' '${SoftFever_VERSION}'\n")
|
||||
|
||||
set(CPACK_WIX_UPGRADE_GUID "058245e8-20e0-4a95-9ab7-1acfe17ad511")
|
||||
set(CPACK_GENERATOR NSIS)
|
||||
|
||||
@@ -45,28 +45,10 @@
|
||||
<color type="primary" scheme_preference="dark">#00695C</color>
|
||||
</branding>
|
||||
<releases>
|
||||
<release version="2.5.0-alpha" date="2026-10-14" type="development">
|
||||
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_5_0_alpha</url>
|
||||
<release version="2.5.0-dev" date="2026-06-22" type="development">
|
||||
<url type="details">https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds</url>
|
||||
<description>
|
||||
<p>See the release page for detailed changelog of OrcaSlicer 2.5.0-alpha release.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.4.2" date="2026-07-26">
|
||||
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_2</url>
|
||||
<description>
|
||||
<p>See the release page for detailed changelog of OrcaSlicer 2.4.2 release.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.4.1" date="2026-06-26">
|
||||
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_1</url>
|
||||
<description>
|
||||
<p>See the release page for detailed changelog of OrcaSlicer 2.4.1 release.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.4.0" date="2026-06-20">
|
||||
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_0</url>
|
||||
<description>
|
||||
<p>See the release page for detailed changelog of OrcaSlicer 2.4.0 release.</p>
|
||||
<p>See the release page for detailed changelog.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.3.2" date="2025-03-23">
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$ArtifactDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-RelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Root,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$rootWithSeparator = $Root.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
||||
$rootUri = [Uri]$rootWithSeparator
|
||||
$pathUri = [Uri]$Path
|
||||
|
||||
[Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace("/", [IO.Path]::DirectorySeparatorChar)
|
||||
}
|
||||
|
||||
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
|
||||
$binaries = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
|
||||
Where-Object { $_.Extension -in @(".exe", ".dll") } |
|
||||
Sort-Object -Property FullName
|
||||
|
||||
if (-not $binaries) {
|
||||
Write-Warning "No .exe or .dll files found under '$artifactRoot'."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$binaries | ForEach-Object {
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||
|
||||
[pscustomobject]@{
|
||||
RelativePath = Get-RelativePath -Root $artifactRoot -Path $_.FullName
|
||||
Status = $signature.Status
|
||||
SignatureType = $signature.SignatureType
|
||||
Signer = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" }
|
||||
SignerThumbprint = if ($signature.SignerCertificate) { $signature.SignerCertificate.Thumbprint } else { "" }
|
||||
Timestamped = [bool]$signature.TimeStamperCertificate
|
||||
TimeStamper = if ($signature.TimeStamperCertificate) { $signature.TimeStamperCertificate.Subject } else { "" }
|
||||
}
|
||||
} | Format-Table -AutoSize
|
||||
@@ -1,40 +0,0 @@
|
||||
# Copies the files SignPath signed back over the originals in the portable bundle they
|
||||
# were staged from with stage-for-signpath.ps1 -Bundle: the root of the signed artifact
|
||||
# holds OrcaSlicer's own binaries, third-party/ the other files under their paths in the
|
||||
# bundle.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$SignedDirectory,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$Bundle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$signedRoot = (Resolve-Path -LiteralPath $SignedDirectory).Path.TrimEnd('\', '/')
|
||||
$thirdPartyPrefix = "third-party" + [IO.Path]::DirectorySeparatorChar
|
||||
$files = Get-ChildItem -LiteralPath $signedRoot -Recurse -File
|
||||
if (-not $files) {
|
||||
throw "No signed files found in '$signedRoot'."
|
||||
}
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relative = $file.FullName.Substring($signedRoot.Length + 1)
|
||||
if ($relative.StartsWith($thirdPartyPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$relative = $relative.Substring($thirdPartyPrefix.Length)
|
||||
}
|
||||
|
||||
# Every signed file replaces one that was staged, so a file without an original means
|
||||
# the artifact does not have the layout stage-for-signpath.ps1 produces.
|
||||
$target = Join-Path $Bundle $relative
|
||||
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
|
||||
throw "Signed file '$relative' has no original in '$Bundle'."
|
||||
}
|
||||
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
|
||||
}
|
||||
|
||||
Write-Host "Restored $($files.Count) signed files into '$Bundle'."
|
||||
@@ -1,81 +0,0 @@
|
||||
# Copies the files a SignPath signing request covers into the directory that request
|
||||
# uploads, after checking their version information. SignPath refuses to sign a file
|
||||
# whose ProductName or ProductVersion differs from its artifact configuration (see
|
||||
# signpath/), and reports it only once the request has been approved, so the check runs
|
||||
# before anything is submitted.
|
||||
#
|
||||
# -Path stages the given files at the root of -Destination.
|
||||
#
|
||||
# -Bundle stages a portable OrcaSlicer bundle for signpath/windows-binaries-v1.xml:
|
||||
# OrcaSlicer's own binaries (-OwnBinaries) at the root, where their version information
|
||||
# is checked, and every other executable, DLL and Python extension module that carries no
|
||||
# signature under third-party/, at its path in the bundle. Files signed by their vendor,
|
||||
# such as the Microsoft runtime, keep that signature. restore-from-signpath.ps1 copies the
|
||||
# signed files back.
|
||||
[CmdletBinding(DefaultParameterSetName = "Path")]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ParameterSetName = "Path")]
|
||||
[string[]]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true, ParameterSetName = "Bundle")]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$Bundle,
|
||||
|
||||
[Parameter(ParameterSetName = "Bundle")]
|
||||
[string[]]$OwnBinaries = @("orca-slicer.exe", "OrcaSlicer.dll"),
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProductVersion,
|
||||
|
||||
[string]$ProductName = "OrcaSlicer"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Test-VersionInfo {
|
||||
param([string]$File)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $File -PathType Leaf)) {
|
||||
throw "File to sign was not found: $File"
|
||||
}
|
||||
|
||||
$info = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $File).Path)
|
||||
if ($info.ProductName -ne $ProductName -or $info.ProductVersion -ne $ProductVersion) {
|
||||
throw "'$File' has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)'; SignPath expects '$ProductName' and '$ProductVersion'."
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
|
||||
|
||||
if ($PSCmdlet.ParameterSetName -eq "Path") {
|
||||
foreach ($file in $Path) {
|
||||
Test-VersionInfo $file
|
||||
Copy-Item -LiteralPath $file -Destination $Destination -Force
|
||||
Write-Host "Staged $(Split-Path -Leaf $file) ($ProductName $ProductVersion)"
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path.TrimEnd('\', '/')
|
||||
foreach ($name in $OwnBinaries) {
|
||||
$file = Join-Path $bundleRoot $name
|
||||
Test-VersionInfo $file
|
||||
Copy-Item -LiteralPath $file -Destination $Destination -Force
|
||||
Write-Host "Staged $name ($ProductName $ProductVersion)"
|
||||
}
|
||||
|
||||
$thirdParty = Get-ChildItem -LiteralPath $bundleRoot -Recurse -File |
|
||||
Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } |
|
||||
Where-Object { $_.DirectoryName -ne $bundleRoot -or $_.Name -notin $OwnBinaries } |
|
||||
Where-Object { (Get-AuthenticodeSignature -LiteralPath $_.FullName).Status -eq "NotSigned" }
|
||||
|
||||
foreach ($file in $thirdParty) {
|
||||
$relative = $file.FullName.Substring($bundleRoot.Length + 1)
|
||||
$target = Join-Path (Join-Path $Destination "third-party") $relative
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null
|
||||
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
|
||||
}
|
||||
Write-Host "Staged $(@($thirdParty).Count) unsigned third-party files under third-party/"
|
||||
@@ -1,121 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
|
||||
[string]$ArtifactDirectory,
|
||||
|
||||
# Paths relative to -ArtifactDirectory. Without them every executable, DLL and Python
|
||||
# extension module under it is verified.
|
||||
[string[]]$Files,
|
||||
|
||||
[string]$SignToolPath,
|
||||
|
||||
# Accept signatures whose certificate chain terminates in an untrusted root.
|
||||
# Required for the SignPath test certificate (self-signed). Do NOT pass this
|
||||
# with the release certificate, so release builds enforce a fully trusted chain.
|
||||
[switch]$AllowUntrustedRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-SignToolPath {
|
||||
param(
|
||||
[string]$ExplicitPath
|
||||
)
|
||||
|
||||
if ($ExplicitPath) {
|
||||
if (Test-Path -LiteralPath $ExplicitPath -PathType Leaf) {
|
||||
return (Resolve-Path -LiteralPath $ExplicitPath).Path
|
||||
}
|
||||
|
||||
throw "SignTool was not found at '$ExplicitPath'."
|
||||
}
|
||||
|
||||
$fromPath = Get-Command -Name "signtool.exe" -ErrorAction SilentlyContinue
|
||||
if ($fromPath) {
|
||||
return $fromPath.Source
|
||||
}
|
||||
|
||||
$candidateRoots = @(
|
||||
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
|
||||
"${env:ProgramFiles}\Windows Kits\10\bin"
|
||||
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) }
|
||||
|
||||
foreach ($root in $candidateRoots) {
|
||||
$candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter "signtool.exe" -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -match "\\(x64|arm64)\\signtool\.exe$" } |
|
||||
Sort-Object -Property FullName -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($candidate) {
|
||||
return $candidate.FullName
|
||||
}
|
||||
}
|
||||
|
||||
throw "signtool.exe was not found. Install the Windows SDK or pass -SignToolPath."
|
||||
}
|
||||
|
||||
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path.TrimEnd('\', '/')
|
||||
$signtool = Resolve-SignToolPath -ExplicitPath $SignToolPath
|
||||
|
||||
if (-not $Files) {
|
||||
$Files = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
|
||||
Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } |
|
||||
ForEach-Object { $_.FullName.Substring($artifactRoot.Length + 1) }
|
||||
if (-not $Files) {
|
||||
throw "No executables or DLLs found in '$artifactRoot'."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Using SignTool: $signtool"
|
||||
Write-Host "Verifying Authenticode signatures of $(@($Files).Count) files in: $artifactRoot"
|
||||
|
||||
foreach ($relativePath in $Files) {
|
||||
$filePath = Join-Path $artifactRoot $relativePath
|
||||
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) {
|
||||
throw "Expected signed file was not found: $filePath"
|
||||
}
|
||||
|
||||
# Capture signtool output without letting native stderr (redirected via 2>&1)
|
||||
# raise a terminating NativeCommandError under $ErrorActionPreference = 'Stop'.
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$output = & $signtool verify /pa /all /tw /v $filePath 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $filePath
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Verified $relativePath ($($signature.SignerCertificate.Subject))"
|
||||
continue
|
||||
}
|
||||
|
||||
if ($AllowUntrustedRoot) {
|
||||
# signtool interleaves its stdout (chain details) and stderr (the error)
|
||||
# unpredictably, so the error text cannot be matched reliably. Use the
|
||||
# structured Get-AuthenticodeSignature result instead: accept only when the
|
||||
# file is genuinely signed and the sole problem is that the chain terminates
|
||||
# in an untrusted root (i.e. the self-signed SignPath test certificate).
|
||||
$isSigned = ($signature.SignatureType -eq "Authenticode") -and ($null -ne $signature.SignerCertificate)
|
||||
$untrustedRootOnly = $signature.StatusMessage -match "terminated in a root certificate which is not trusted"
|
||||
if ($isSigned -and $untrustedRootOnly) {
|
||||
Write-Host "Accepted $relativePath ($($signature.SignerCertificate.Subject)): signed, but its certificate chains to an untrusted root (expected for the SignPath test certificate)."
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
# The full chain and error details only for a file that did not verify.
|
||||
$output | ForEach-Object { Write-Host " $_" }
|
||||
throw "SignTool verification failed for '$relativePath' with exit code $exitCode."
|
||||
}
|
||||
|
||||
Write-Host "Authenticode verification passed."
|
||||
|
||||
# signtool exits non-zero for the untrusted test-cert root even when we accept
|
||||
# it above, leaving $LASTEXITCODE = 1. The GitHub Actions pwsh wrapper exits
|
||||
# with that lingering code, so reset it to report success explicitly.
|
||||
exit 0
|
||||
@@ -1,21 +0,0 @@
|
||||
# SignPath configurations
|
||||
|
||||
Windows builds of `release/*` branches in `OrcaSlicer/OrcaSlicer` are code signed through [SignPath](https://about.signpath.io/), with the certificate the SignPath Foundation issues to open source projects. The XML files here are copies of the project's artifact configurations in SignPath, which the workflow refers to by slug only; a change to one has to be made in SignPath too, under the same slug.
|
||||
|
||||
Every signing request uses the `release-signing` policy and waits in SignPath until it is approved, so each Windows build of a release branch needs two approvals per architecture: the binaries, then the installer. Builds of `main` and pull requests are not signed.
|
||||
|
||||
## What gets signed
|
||||
|
||||
Every executable, DLL and Python extension module in the bundle ends up with a valid signature, so Windows features that check each loaded binary, such as Smart App Control, find none unsigned. Files their vendor already signed keep that signature: the Microsoft runtime DLLs and `WebView2Loader.dll` (Microsoft) and CPython's bundled OpenSSL and libffi (Python Software Foundation). Everything else is signed with the project's certificate: OrcaSlicer's own `orca-slicer.exe` and `OrcaSlicer.dll`, the libraries built from upstream sources (OCCT, FFmpeg, FreeType, GMP, MPFR, CPython), the downloaded `uv.exe`, and the NSIS installer.
|
||||
|
||||
The SignPath Foundation's conditions ask projects to sign only binaries built from their own source and to leave upstream projects' binaries unsigned, which they allow inside a signed installer. Signing the upstream libraries departs from that, by the maintainers' choice. Narrowing the signed set back to OrcaSlicer's own files means dropping the `third-party` file set from `windows-binaries-v1` and the third-party staging from `scripts/stage-for-signpath.ps1`.
|
||||
|
||||
The Foundation also requires metadata restrictions on signed files. OrcaSlicer's own binaries and the installer must have the ProductName `OrcaSlicer` and the ProductVersion passed in with the request, which the workflow takes from `SoftFever_VERSION` in `version.inc`. The binaries get that version information from `src/dev-utils/platform/msw/OrcaSlicer-version.rc.in`, the installer from `CPACK_NSIS_DEFINES` in `CMakeLists.txt`. The upstream files carry their own projects' metadata, so their file set has no restriction. `scripts/stage-for-signpath.ps1` checks the version information before a request is submitted, since SignPath only rejects a mismatch after the request has been approved.
|
||||
|
||||
## `windows-binaries-v1`
|
||||
|
||||
Signs the binaries of the installed bundle, `${{ env.BUILD_DIR }}/OrcaSlicer`. `scripts/stage-for-signpath.ps1 -Bundle` stages them for upload: `orca-slicer.exe` and `OrcaSlicer.dll` at the root of the artifact ZIP, where the metadata restrictions apply, and every other unsigned `.exe`, `.dll` and `.pyd` under `third-party/`, at its path in the bundle. `scripts/restore-from-signpath.ps1` copies the signed files back over the originals. The portable ZIP, the MSIX package and the installer are all packed from that bundle; `cpack` is pointed at it with `CPACK_INSTALLED_DIRECTORIES` rather than re-running the install rules, which would take CPython and `uv.exe` from their unsigned sources again. A check after `cpack` verifies that every binary in the installer's payload is signed.
|
||||
|
||||
## `windows-installer-v1`
|
||||
|
||||
Signs the NSIS installer `cpack` produces, `OrcaSlicer_Windows_Installer_<version>_<arch>.exe`, uploaded as the only file of its artifact. The uninstaller NSIS writes during installation is not signed.
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<zip-file>
|
||||
<pe-file-set product-name="OrcaSlicer" product-version="${version}">
|
||||
<include path="orca-slicer.exe" />
|
||||
<include path="OrcaSlicer.dll" />
|
||||
<for-each>
|
||||
<authenticode-sign />
|
||||
</for-each>
|
||||
</pe-file-set>
|
||||
<directory path="third-party">
|
||||
<pe-file-set>
|
||||
<include path="**/*.exe" min-matches="0" max-matches="unbounded" />
|
||||
<include path="**/*.dll" min-matches="0" max-matches="unbounded" />
|
||||
<include path="**/*.pyd" min-matches="0" max-matches="unbounded" />
|
||||
<for-each>
|
||||
<authenticode-sign />
|
||||
</for-each>
|
||||
</pe-file-set>
|
||||
</directory>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<zip-file>
|
||||
<pe-file path="OrcaSlicer_Windows_Installer_*.exe" product-name="OrcaSlicer" product-version="${version}">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
+2
-6
@@ -140,11 +140,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.rc.
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.manifest.in ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.manifest @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/osx/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY)
|
||||
if (WIN32)
|
||||
foreach (_orca_rc_binary IN ITEMS orca-slicer.exe OrcaSlicer.dll)
|
||||
set(ORCA_RC_ORIGINAL_FILENAME ${_orca_rc_binary})
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer-version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/${_orca_rc_binary}.version.rc @ONLY)
|
||||
endforeach ()
|
||||
add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.dll.version.rc)
|
||||
add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp)
|
||||
else ()
|
||||
add_executable(OrcaSlicer OrcaSlicer.cpp OrcaSlicer.hpp)
|
||||
endif ()
|
||||
@@ -220,7 +216,7 @@ if (WIN32)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode")
|
||||
endif()
|
||||
|
||||
add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc ${CMAKE_CURRENT_BINARY_DIR}/orca-slicer.exe.version.rc)
|
||||
add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc)
|
||||
# Generate debug symbols even in release mode.
|
||||
if(MSVC)
|
||||
target_link_options(OrcaSlicer_app_gui PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Version information of orca-slicer.exe and OrcaSlicer.dll, configured once per binary.
|
||||
// SignPath only signs a binary whose ProductName and ProductVersion match its artifact
|
||||
// configuration (signpath/windows-binaries-v1.xml), which the workflow passes the
|
||||
// SoftFever_VERSION from version.inc.
|
||||
1 VERSIONINFO
|
||||
FILEVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0
|
||||
PRODUCTVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0
|
||||
{
|
||||
BLOCK "StringFileInfo"
|
||||
{
|
||||
BLOCK "040904E4"
|
||||
{
|
||||
VALUE "CompanyName", "SoftFever"
|
||||
VALUE "FileDescription", "@SLIC3R_APP_NAME@"
|
||||
VALUE "FileVersion", "@SoftFever_VERSION@"
|
||||
VALUE "ProductName", "@SLIC3R_APP_NAME@"
|
||||
VALUE "ProductVersion", "@SoftFever_VERSION@"
|
||||
VALUE "InternalName", "@SLIC3R_APP_NAME@"
|
||||
VALUE "OriginalFilename", "@ORCA_RC_ORIGINAL_FILENAME@"
|
||||
}
|
||||
}
|
||||
BLOCK "VarFileInfo"
|
||||
{
|
||||
VALUE "Translation", 0x409, 1252
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,25 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION @SLIC3R_VERSION@
|
||||
PRODUCTVERSION @SLIC3R_VERSION@
|
||||
{
|
||||
BLOCK "StringFileInfo"
|
||||
{
|
||||
BLOCK "040904E4"
|
||||
{
|
||||
VALUE "CompanyName", "SoftFever"
|
||||
VALUE "FileDescription", "@SLIC3R_APP_NAME@"
|
||||
VALUE "FileVersion", "@SLIC3R_BUILD_ID@"
|
||||
VALUE "ProductName", "@SLIC3R_APP_NAME@"
|
||||
VALUE "ProductVersion", "@SLIC3R_BUILD_ID@"
|
||||
VALUE "InternalName", "@SLIC3R_APP_NAME@"
|
||||
VALUE "LegalCopyright", ""
|
||||
VALUE "OriginalFilename", "orca-slicer.exe"
|
||||
}
|
||||
}
|
||||
BLOCK "VarFileInfo"
|
||||
{
|
||||
VALUE "Translation", 0x409, 1252
|
||||
}
|
||||
}
|
||||
2 ICON "@SLIC3R_RESOURCES_DIR@/images/OrcaSlicer.ico"
|
||||
1 24 "OrcaSlicer.manifest"
|
||||
@@ -211,35 +211,6 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
|
||||
return decode_colored_png(stream, out_img);
|
||||
}
|
||||
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
struct ReadBufStream stream{pngbuf};
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
stream.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!dsc.png)
|
||||
return false;
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if (!dsc.info)
|
||||
return false;
|
||||
|
||||
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
|
||||
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
|
||||
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
|
||||
bit_depth == 8 && (grey || colour);
|
||||
}
|
||||
|
||||
|
||||
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
|
||||
// Based on https://www.lemoda.net/c/write-png/
|
||||
|
||||
@@ -50,9 +50,6 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
|
||||
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
|
||||
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref;
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
|
||||
return decode_height_texture(layer).has_color();
|
||||
}
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
if (layer.empty())
|
||||
@@ -351,15 +351,13 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
}
|
||||
}
|
||||
|
||||
if (keep_decoded) {
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||
auto &entries = g_decoded_texture_cache.entries;
|
||||
for (auto it = entries.begin(); it != entries.end();)
|
||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
|
||||
@@ -599,9 +599,8 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
|
||||
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
||||
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
|
||||
// as it did when the importer flattened it to grey on the way in. Returns an empty
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
|
||||
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
||||
|
||||
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
||||
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
|
||||
|
||||
@@ -842,13 +842,6 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
|
||||
|
||||
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
|
||||
|
||||
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
|
||||
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
|
||||
// is made absolute and lexically normalized first, because the prefix disables normalisation.
|
||||
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
|
||||
// or when an absolute path cannot be formed, the input is returned unchanged.
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#if WIN32
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
@@ -63,11 +62,7 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
||||
|
||||
FILE *f = nullptr;
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
||||
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
||||
const std::wstring wide_path = boost::nowide::widen(fname);
|
||||
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
||||
f = boost::nowide::fopen(extended.c_str(), mode);
|
||||
f = boost::nowide::fopen(fname, mode);
|
||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||
f = fopen64(fname, mode);
|
||||
#else
|
||||
@@ -106,14 +101,6 @@ bool close_zip(mz_zip_archive *zip, bool isread)
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// mz_file_write_func writing an extracted entry into an already-open FILE*.
|
||||
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
|
||||
{
|
||||
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
|
||||
@@ -129,36 +116,6 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
||||
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
|
||||
{
|
||||
if (!zip)
|
||||
return false;
|
||||
#ifdef WIN32
|
||||
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
|
||||
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
|
||||
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
|
||||
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
|
||||
if (!file) {
|
||||
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !closed) {
|
||||
if (written && !closed)
|
||||
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
|
||||
// Drop the partial entry rather than leave a truncated file behind.
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
|
||||
{
|
||||
if (stat.m_is_utf8)
|
||||
@@ -199,13 +156,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
}
|
||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
||||
try {
|
||||
// Use the extended-length form for every filesystem call so entries under a destination
|
||||
// past MAX_PATH are created even with LongPathsEnabled off.
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
const boost::filesystem::path io_dest = extended_path(dest_path);
|
||||
if (stat.m_is_directory) {
|
||||
if (!boost::filesystem::exists(io_dest))
|
||||
boost::filesystem::create_directories(io_dest);
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
@@ -213,9 +167,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
||||
continue;
|
||||
}
|
||||
// Replace a symlink at the destination rather than writing through it.
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
||||
boost::filesystem::remove(io_dest);
|
||||
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
||||
boost::filesystem::remove(dest_path);
|
||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef MINIZ_EXTENSION_HPP
|
||||
#define MINIZ_EXTENSION_HPP
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <string>
|
||||
#include <miniz.h>
|
||||
|
||||
@@ -14,10 +13,6 @@ bool close_zip_writer(mz_zip_archive *zip);
|
||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
||||
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
|
||||
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
|
||||
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
|
||||
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
|
||||
|
||||
class MZ_Archive {
|
||||
public:
|
||||
|
||||
@@ -2254,45 +2254,5 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
|
||||
return mod >= 0 && mod < count;
|
||||
}
|
||||
|
||||
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
|
||||
#ifdef WIN32
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
|
||||
// be absolutized or normalized): leave it alone.
|
||||
{
|
||||
fs::path prefix_candidate = path;
|
||||
const std::wstring native = prefix_candidate.make_preferred().wstring();
|
||||
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
|
||||
return fs::path(native);
|
||||
}
|
||||
|
||||
fs::path absolute = path;
|
||||
if (!absolute.is_absolute()) {
|
||||
boost::system::error_code ec;
|
||||
absolute = fs::absolute(absolute, ec);
|
||||
if (ec)
|
||||
return path; // can't form a valid prefixed path from a relative one
|
||||
}
|
||||
|
||||
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
|
||||
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
|
||||
while (absolute.has_relative_path() && absolute.filename() == ".")
|
||||
absolute = absolute.parent_path();
|
||||
|
||||
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
|
||||
absolute = absolute.lexically_normal();
|
||||
|
||||
// Extended paths only recognize backslash separators.
|
||||
const std::wstring native = absolute.make_preferred().wstring();
|
||||
if (native.empty())
|
||||
return path;
|
||||
if (native.rfind(L"\\\\", 0) == 0)
|
||||
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
|
||||
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
|
||||
#else
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
@@ -174,10 +174,6 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048;
|
||||
// The full-size image is only ever adjusted once, on commit.
|
||||
constexpr int ADJUST_PREVIEW_MAX_PX = 384;
|
||||
|
||||
// Library textures the texture picker decodes per frame. Each decode runs on the GUI thread, so the
|
||||
// rest of the visible tiles fill in over the following frames instead of in one stall.
|
||||
constexpr int PICKER_LOADS_PER_FRAME = 2;
|
||||
|
||||
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
|
||||
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own
|
||||
// cost growing to matter: it is a ring around a perimeter, not an area.
|
||||
@@ -4122,7 +4118,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge
|
||||
if (entry.image_data) {
|
||||
TextureDisplacementLayer probe;
|
||||
probe.image_data = entry.image_data;
|
||||
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
|
||||
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe));
|
||||
if (!entry.thumbnail)
|
||||
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
|
||||
} else {
|
||||
@@ -4335,8 +4331,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
std::optional<TextureLibraryEntry> to_remove;
|
||||
bool import_clicked = false;
|
||||
bool download_clicked = false;
|
||||
int loads_left = PICKER_LOADS_PER_FRAME;
|
||||
bool loads_deferred = false;
|
||||
|
||||
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
|
||||
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
|
||||
@@ -4354,30 +4348,11 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
};
|
||||
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
|
||||
// name, the user's own most recently used first - the order texture_library() produced); a user tile
|
||||
// carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
|
||||
// carries a remove button on hover. Returns how many it drew.
|
||||
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
|
||||
int n = 0;
|
||||
for (const TextureLibraryEntry *entry : entries) {
|
||||
const ImVec2 rect_min = tile_pos(origin, n++);
|
||||
const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile);
|
||||
const bool current = entry->path == layer->path;
|
||||
// The selected tile is centred even when it is off screen, so it is drawn and loaded next frame.
|
||||
if (current && m_picker_reveal_selected) {
|
||||
ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f);
|
||||
m_parent.request_extra_frame();
|
||||
}
|
||||
if (!ImGui::IsRectVisible(rect_min, rect_max))
|
||||
continue;
|
||||
// Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames.
|
||||
const bool cached = m_library_textures.count(entry->path) != 0;
|
||||
if (!cached && loads_left == 0) {
|
||||
loads_deferred = true;
|
||||
dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
|
||||
continue;
|
||||
}
|
||||
if (!cached)
|
||||
--loads_left;
|
||||
ImGui::SetCursorScreenPos(rect_min);
|
||||
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
|
||||
ImGui::PushID(entry->path.c_str());
|
||||
const LibraryTexture *tex = get_library_texture(entry->path);
|
||||
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
|
||||
@@ -4400,6 +4375,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
if (removable)
|
||||
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
|
||||
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
|
||||
const bool current = entry->path == layer->path;
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
|
||||
// fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
|
||||
@@ -4408,6 +4384,9 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
|
||||
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
|
||||
}
|
||||
// Centred, as far as the scroll range allows, so the textures around it are in view too.
|
||||
if (current && m_picker_reveal_selected)
|
||||
ImGui::SetScrollFromPosY(0.5f * (a.y + b.y) - ImGui::GetWindowPos().y, 0.5f);
|
||||
if (hovered)
|
||||
panel_tooltip(entry->name);
|
||||
|
||||
@@ -4596,8 +4575,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
if (loads_deferred)
|
||||
m_parent.request_extra_frame();
|
||||
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
|
||||
m_picker_reveal_selected = false;
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -223,11 +223,23 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
|
||||
// Anything else is converted on load.
|
||||
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
|
||||
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
|
||||
// the height and whose RGB is available to colour the model). Anything else is converted on load.
|
||||
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
|
||||
if (bytes.empty())
|
||||
return false;
|
||||
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
|
||||
if (!png::is_png(rbuf))
|
||||
return false;
|
||||
png::ImageGreyscale gray;
|
||||
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
|
||||
return true;
|
||||
png::ImageColorscale color;
|
||||
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
|
||||
color.bytes_per_pixel >= 3 &&
|
||||
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
|
||||
}
|
||||
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
const char* const INSTALL_STATE_FILE = ".install_state.json";
|
||||
@@ -661,7 +665,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(extended_path(destination), ec);
|
||||
fs::create_directories(destination, ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin staging directory: " + ec.message();
|
||||
return false;
|
||||
@@ -705,7 +709,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
|
||||
const fs::path output_path = destination / relative_path;
|
||||
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
|
||||
fs::create_directories(extended_path(output_path), ec);
|
||||
fs::create_directories(output_path, ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
|
||||
return false;
|
||||
@@ -713,19 +717,26 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories(extended_path(output_path.parent_path()), ec);
|
||||
fs::create_directories(output_path.parent_path(), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
|
||||
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
|
||||
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shared with the rest of the archive code: long-path capable and binary-safe.
|
||||
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
|
||||
const std::string encoded_output_path = encode_path(output_path.string().c_str());
|
||||
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
|
||||
#ifdef WIN32
|
||||
if (!extracted) {
|
||||
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
|
||||
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
|
||||
}
|
||||
#endif
|
||||
if (!extracted) {
|
||||
error = "Failed to extract plugin zip entry " + relative_key + ": " +
|
||||
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
|
||||
@@ -584,6 +584,19 @@ bool PythonInterpreter::initialize()
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
|
||||
|
||||
// Keep bytecode out of the Python home. The shipped .pyc files are stale against their
|
||||
// sources, so a writable install would rewrite them inside the app (breaking a signed
|
||||
// macOS bundle's seal) and a read-only one would recompile them on every launch.
|
||||
const std::string pycache_prefix = data_dir() + "/python/pycache";
|
||||
#ifdef _WIN32
|
||||
status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
|
||||
#else
|
||||
status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
|
||||
#endif
|
||||
if (PyStatus_Exception(status))
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
|
||||
<< (status.err_msg ? status.err_msg : "unknown error");
|
||||
|
||||
// Use pybind11's scoped_interpreter which properly initializes
|
||||
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
|
||||
// does not set up thread state tracking that pybind11 requires.
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <miniz.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -177,55 +176,6 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
|
||||
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
||||
}
|
||||
|
||||
TEST_CASE("Confined extraction writes files under a target path longer than MAX_PATH", "[MinizExtension]")
|
||||
{
|
||||
ScopedTemporaryDir tmp;
|
||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
||||
// Directory entries are explicit: extract_archive_confined recreates directories only for them.
|
||||
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"},
|
||||
{"vendor/machine/printer.json", "{\"b\":2}"}});
|
||||
|
||||
// One 240-char component keeps every name below the NTFS 255 limit while pushing the full
|
||||
// target path past the 260-char MAX_PATH the Win32 APIs enforce when LongPathsEnabled is off.
|
||||
const fs::path target = tmp.path() / std::string(240, 'd');
|
||||
REQUIRE(target.string().size() > 260);
|
||||
fs::create_directories(extended_path(target));
|
||||
|
||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
||||
// Verify through extended-length paths, otherwise the checks themselves are bounded by MAX_PATH.
|
||||
CHECK(fs::is_directory(extended_path(target / "vendor")));
|
||||
CHECK(read_file(extended_path(target / "vendor.json")) == "{\"a\":1}");
|
||||
CHECK(read_file(extended_path(target / "vendor" / "machine" / "printer.json")) == "{\"b\":2}");
|
||||
|
||||
// The temporary dir guard cannot remove a deep tree on Windows, so drop it here.
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(extended_path(target), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
TEST_CASE("A zip archive under a path longer than MAX_PATH can be written and read", "[MinizExtension]")
|
||||
{
|
||||
ScopedTemporaryDir tmp;
|
||||
|
||||
// Create the long directory with the extended-length path, then exercise the archive
|
||||
// reader/writer against an archive whose own path is past MAX_PATH.
|
||||
const fs::path long_dir = tmp.path() / std::string(240, 'z');
|
||||
fs::create_directories(extended_path(long_dir));
|
||||
const fs::path zip_file = long_dir / "bundle.zip";
|
||||
REQUIRE(zip_file.string().size() > 260);
|
||||
|
||||
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
|
||||
|
||||
const fs::path target = tmp.path() / "out";
|
||||
fs::create_directories(target);
|
||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
||||
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(extended_path(long_dir), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
|
||||
{
|
||||
|
||||
@@ -521,49 +521,3 @@ TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]")
|
||||
CHECK_FALSE(ascii_iequals("[", "{"));
|
||||
CHECK_FALSE(ascii_iequals("@", "`"));
|
||||
}
|
||||
|
||||
TEST_CASE("extended_path turns a Windows path into its long-path form", "[Utils]") {
|
||||
#ifdef _WIN32
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
SECTION("a normal absolute drive path is prefixed") {
|
||||
CHECK(extended_path(fs::path(R"(C:\models\model.3mf)")).wstring() == LR"(\\?\C:\models\model.3mf)");
|
||||
}
|
||||
SECTION("separators are made native") {
|
||||
CHECK(extended_path(fs::path("C:/models/model.3mf")).wstring() == LR"(\\?\C:\models\model.3mf)");
|
||||
}
|
||||
SECTION("dot segments are collapsed before prefixing") {
|
||||
// The "\\?\" prefix disables Win32 normalisation, so "." and ".." must be gone beforehand.
|
||||
CHECK(extended_path(fs::path(R"(C:\models\..\model.3mf)")).wstring() == LR"(\\?\C:\model.3mf)");
|
||||
}
|
||||
SECTION("a trailing separator is dropped, not turned into a dot element") {
|
||||
CHECK(extended_path(fs::path(R"(C:\models\)")).wstring() == LR"(\\?\C:\models)");
|
||||
}
|
||||
SECTION("a UNC path becomes an extended UNC path") {
|
||||
CHECK(extended_path(fs::path(R"(\\server\share\model.3mf)")).wstring() == LR"(\\?\UNC\server\share\model.3mf)");
|
||||
}
|
||||
SECTION("an already extended path is returned unchanged") {
|
||||
const fs::path extended(R"(\\?\C:\models\model.3mf)");
|
||||
CHECK(extended_path(extended) == extended);
|
||||
}
|
||||
SECTION("a device path is returned unchanged") {
|
||||
const fs::path device(R"(\\.\C:\models\model.3mf)");
|
||||
CHECK(extended_path(device) == device);
|
||||
}
|
||||
SECTION("a bare device path without a root directory is returned unchanged") {
|
||||
// Not "absolute" by Boost's rules, so it must be recognized before absolutizing.
|
||||
const fs::path device(R"(\\.\PhysicalDrive0)");
|
||||
CHECK(extended_path(device) == device);
|
||||
}
|
||||
SECTION("a relative path is completed against the working directory") {
|
||||
const fs::path result = extended_path(fs::path("model.3mf"));
|
||||
CHECK(result.wstring().rfind(L"\\\\?\\", 0) == 0);
|
||||
CHECK(result.filename() == fs::path("model.3mf"));
|
||||
}
|
||||
#else
|
||||
SECTION("non-Windows returns the path unchanged") {
|
||||
const boost::filesystem::path p = boost::filesystem::temp_directory_path() / "model.3mf";
|
||||
CHECK(extended_path(p) == p);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <ios>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <slic3r/plugin/PluginLoader.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
@@ -11,17 +10,13 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include "catch2/catch_message.hpp"
|
||||
#include "plugin_test_utils.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <libslic3r/Zipper.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -36,14 +31,6 @@ fs::path write_py_file(const fs::path& dir, const std::string& filename, const s
|
||||
return p;
|
||||
}
|
||||
|
||||
// The shared production helper: makes the path absolute and lexically normalized and, on
|
||||
// Windows, prefixes it with the extended-length "\\?\" form, so the test can create, verify and
|
||||
// clean up a tree past MAX_PATH even with LongPathsEnabled off. Identity on other platforms.
|
||||
fs::path to_extended_path(const fs::path& p)
|
||||
{
|
||||
return Slic3r::extended_path(p);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]")
|
||||
@@ -197,59 +184,3 @@ TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginIns
|
||||
REQUIRE(read_install_state(plugin_dir, scanned));
|
||||
CHECK(scanned.installed_version == "1.0.0");
|
||||
}
|
||||
|
||||
TEST_CASE("extract_zip_to_directory writes entries under a destination longer than MAX_PATH",
|
||||
"[PluginInstall][PluginFsUtils]")
|
||||
{
|
||||
ScopedTemporaryDir dir("deep-extract");
|
||||
|
||||
const fs::path wheel = dir.path() / "deep_pkg-1.0-py3-none-any.whl";
|
||||
const std::string payload = "print('hello from a deep path')\n";
|
||||
{
|
||||
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
|
||||
zipper.add_entry("pkg/", "", 0); // explicit directory entry
|
||||
zipper.add_entry("pkg/module.py", payload.data(), payload.size());
|
||||
zipper.finalize();
|
||||
}
|
||||
|
||||
// A single 240-char component keeps every filename below the NTFS 255 limit while pushing the
|
||||
// full destination (temp dir + component) past the 260-char MAX_PATH the Win32 APIs enforce
|
||||
// when LongPathsEnabled is off.
|
||||
const fs::path destination = dir.path() / std::string(240, 'd');
|
||||
REQUIRE(destination.string().size() > 260);
|
||||
|
||||
std::string error;
|
||||
REQUIRE(extract_zip_to_directory(wheel, destination, error));
|
||||
INFO("extract error: " << error);
|
||||
CHECK(error.empty());
|
||||
|
||||
// Verify through a long-path-capable path, otherwise the check itself is bounded by MAX_PATH.
|
||||
const fs::path module_py = destination / "pkg" / "module.py";
|
||||
REQUIRE(fs::exists(to_extended_path(module_py)));
|
||||
REQUIRE(fs::is_regular_file(to_extended_path(module_py)));
|
||||
|
||||
// Remove the deep subtree here: ScopedTemporaryDir's plain remove_all cannot reach past
|
||||
// MAX_PATH on Windows.
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(to_extended_path(destination), ec);
|
||||
CHECK_FALSE(ec);
|
||||
}
|
||||
|
||||
TEST_CASE("extract_zip_to_directory rejects an entry that escapes the destination",
|
||||
"[PluginInstall][PluginFsUtils]")
|
||||
{
|
||||
ScopedTemporaryDir dir("escape-extract");
|
||||
|
||||
const fs::path wheel = dir.path() / "evil-1.0-py3-none-any.whl";
|
||||
{
|
||||
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
|
||||
zipper.add_entry("pkg/module.py", "print('ok')\n", 12);
|
||||
zipper.add_entry("../escape.txt", "escaped\n", 8);
|
||||
zipper.finalize();
|
||||
}
|
||||
|
||||
std::string error;
|
||||
CHECK_FALSE(extract_zip_to_directory(wheel, dir.path() / "out", error));
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("escapes the plugin package"));
|
||||
CHECK_FALSE(fs::exists(dir.path() / "escape.txt"));
|
||||
}
|
||||
@@ -615,8 +615,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path",
|
||||
|
||||
// Ensure the next import executes the new submodule rather than reusing a stale package child.
|
||||
write_package("'new'");
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(package_root / "__pycache__", ec);
|
||||
{
|
||||
// The rewrite can keep the old mtime and size, so drop the bytecode wherever the
|
||||
// interpreter caches it.
|
||||
PythonGILState gil;
|
||||
REQUIRE(gil);
|
||||
PyObjectPtr util(PyImport_ImportModule("importlib.util"));
|
||||
REQUIRE(util);
|
||||
for (const char* file : {"__init__.py", "sub.py"}) {
|
||||
PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str()));
|
||||
REQUIRE(source);
|
||||
PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get()));
|
||||
REQUIRE(cached);
|
||||
boost::system::error_code ec;
|
||||
fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec);
|
||||
}
|
||||
}
|
||||
|
||||
paths.clear();
|
||||
modules.clear();
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ set(SLIC3R_APP_KEY "OrcaSlicer")
|
||||
if(NOT DEFINED BBL_INTERNAL_TESTING)
|
||||
set(BBL_INTERNAL_TESTING "0")
|
||||
endif()
|
||||
set(SoftFever_VERSION "2.5.0-alpha")
|
||||
set(SoftFever_VERSION "2.5.0-dev")
|
||||
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)"
|
||||
SoftFever_VERSION_MATCH ${SoftFever_VERSION})
|
||||
set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})
|
||||
|
||||
Reference in new issue
Block a user