Compare commits

..
Author SHA1 Message Date
Hanif Koh f307785cad Drop Cached Bytecode Through the Interpreter in the Module Release Test 2026-10-16 19:18:02 +08:00
Hanif Koh 6bc835ea30 Keep Python Bytecode out of the Bundled Python Home
The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.
2026-10-16 16:23:51 +08:00
18 changed files with 86 additions and 419 deletions
-13
View File
@@ -288,19 +288,6 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
-50
View File
@@ -111,54 +111,6 @@ else
export CMAKE_POLICY_COMPAT=""
fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:"
echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -210,7 +162,6 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -313,7 +264,6 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
-29
View File
@@ -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/
-3
View File
@@ -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;
+8 -10
View File
@@ -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,
+2 -3
View File
@@ -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.
-7
View File
@@ -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
+8 -53
View File
@@ -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;
-5
View File
@@ -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:
-40
View File
@@ -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();
+15 -3
View File
@@ -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;
+17 -6
View File
@@ -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;
+13
View File
@@ -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.
-50
View File
@@ -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]")
{
-46
View File
@@ -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
}
-69
View File
@@ -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"));
}
+16 -2
View File
@@ -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();