mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 12:21:11 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82aee1abdd | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
3d07663ab2 | ||
|
|
4149401c53 |
@@ -288,6 +288,19 @@ 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")
|
||||
|
||||
@@ -111,6 +111,54 @@ 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"
|
||||
@@ -162,6 +210,7 @@ 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
|
||||
@@ -264,6 +313,7 @@ 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"
|
||||
|
||||
@@ -211,6 +211,35 @@ 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,6 +50,9 @@ 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)
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||
{
|
||||
DecodedHeightTexture result;
|
||||
if (layer.empty())
|
||||
@@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||
|
||||
@@ -599,8 +599,9 @@ 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.
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
||||
// 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);
|
||||
|
||||
// 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,6 +842,13 @@ 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,6 +17,7 @@
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
|
||||
|
||||
FILE *f = nullptr;
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
f = boost::nowide::fopen(fname, mode);
|
||||
// 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);
|
||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||
f = fopen64(fname, mode);
|
||||
#else
|
||||
@@ -101,6 +106,14 @@ 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)
|
||||
@@ -116,6 +129,36 @@ 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)
|
||||
@@ -156,10 +199,13 @@ 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) {
|
||||
const boost::filesystem::path dest_path(dest_file);
|
||||
if (!boost::filesystem::exists(dest_path))
|
||||
boost::filesystem::create_directories(dest_path);
|
||||
if (!boost::filesystem::exists(io_dest))
|
||||
boost::filesystem::create_directories(io_dest);
|
||||
continue;
|
||||
}
|
||||
if (stat.m_uncomp_size == 0) {
|
||||
@@ -167,10 +213,9 @@ 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.
|
||||
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)) {
|
||||
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)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
||||
close_zip_reader(&archive);
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MINIZ_EXTENSION_HPP
|
||||
#define MINIZ_EXTENSION_HPP
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <string>
|
||||
#include <miniz.h>
|
||||
|
||||
@@ -13,6 +14,10 @@ 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,5 +2254,45 @@ 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,6 +174,10 @@ 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.
|
||||
@@ -4118,7 +4122,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));
|
||||
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
|
||||
if (!entry.thumbnail)
|
||||
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
|
||||
} else {
|
||||
@@ -4331,6 +4335,8 @@ 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
|
||||
@@ -4348,11 +4354,30 @@ 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 it drew.
|
||||
// carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
|
||||
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
|
||||
int n = 0;
|
||||
for (const TextureLibraryEntry *entry : entries) {
|
||||
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
|
||||
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::PushID(entry->path.c_str());
|
||||
const LibraryTexture *tex = get_library_texture(entry->path);
|
||||
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
|
||||
@@ -4375,7 +4400,6 @@ 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.
|
||||
@@ -4384,9 +4408,6 @@ 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);
|
||||
|
||||
@@ -4575,6 +4596,8 @@ 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,23 +223,11 @@ 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 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.
|
||||
// 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.
|
||||
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
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);
|
||||
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
|
||||
}
|
||||
|
||||
std::vector<TextureLibraryEntry> g_library;
|
||||
|
||||
@@ -34,10 +34,6 @@
|
||||
#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";
|
||||
@@ -665,7 +661,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(destination, ec);
|
||||
fs::create_directories(extended_path(destination), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin staging directory: " + ec.message();
|
||||
return false;
|
||||
@@ -709,7 +705,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(output_path, ec);
|
||||
fs::create_directories(extended_path(output_path), ec);
|
||||
if (ec) {
|
||||
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
|
||||
return false;
|
||||
@@ -717,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories(output_path.parent_path(), ec);
|
||||
fs::create_directories(extended_path(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(output_path, ec) && fs::is_directory(output_path, ec)) {
|
||||
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
|
||||
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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)) {
|
||||
error = "Failed to extract plugin zip entry " + relative_key + ": " +
|
||||
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
|
||||
return false;
|
||||
|
||||
@@ -584,19 +584,6 @@ 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,6 +18,7 @@
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <miniz.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -176,6 +177,55 @@ 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,3 +521,49 @@ 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,6 +3,7 @@
|
||||
#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>
|
||||
@@ -10,13 +11,17 @@
|
||||
#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;
|
||||
|
||||
@@ -31,6 +36,14 @@ 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]")
|
||||
@@ -184,3 +197,59 @@ 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,22 +615,8 @@ 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'");
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(package_root / "__pycache__", ec);
|
||||
|
||||
paths.clear();
|
||||
modules.clear();
|
||||
|
||||
Reference in New Issue
Block a user