From d3699dd9f6b292b59a7b01d735a2b53d462c87d7 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:48:53 +0800 Subject: [PATCH] Keep Python Bytecode out of the Bundled Python Home (#16550) * 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 /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. * Drop Cached Bytecode Through the Interpreter in the Module Release Test --- src/slic3r/plugin/PythonInterpreter.cpp | 13 +++++++++++++ tests/slic3rutils/test_plugin_lifecycle.cpp | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index 4a08431117..89dc91723c 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -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. diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 58a4f1a079..498935c2f6 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -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();