Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions include/openscad_cpp_parser/api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,14 @@ struct LibraryFileResult {
LibraryFileResult getASTFromLibraryFile(const std::string& currFile, const std::string& libFile,
bool includeComments = false, bool processIncludes = true);

// OpenSCAD's library search path: (1) directory of currFile, (2)
// OPENSCADPATH env var (':'-separated on POSIX, ';' on Windows), (3)
// platform default library dir.
// OpenSCAD's library search path, in order: (1) directory of currFile,
// (2) each OPENSCADPATH entry (':'-separated on POSIX, ';' on Windows),
// (3) the user's own libraries folder, (4) libraries shipped beside this
// binary. OPENSCADPATH ADDS to (3) and (4) rather than replacing them,
// which is what OpenSCAD's own parser_init() does.
std::vector<std::string> librarySearchDirs(const std::string& currFile);

// The first entry of librarySearchDirs() that holds `libFile`.
std::optional<std::string> findLibraryFile(const std::string& currFile, const std::string& libFile);

// A file's statements with its `include <...>` directives resolved, where
Expand Down
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ target_include_directories(openscad_cpp_parser PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(openscad_cpp_parser PUBLIC nlohmann_json::nlohmann_json)
# SHGetFolderPathW, for the real My Documents rather than an assumed
# %USERPROFILE%\Documents -- see api.cpp userLibraryDir().
if(WIN32)
target_link_libraries(openscad_cpp_parser PUBLIC shell32)
endif()
target_compile_options(openscad_cpp_parser PRIVATE
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall;-Wextra>
)
146 changes: 118 additions & 28 deletions src/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@

#include <cstdlib>
#include <filesystem>
#if defined(_WIN32)
// NOMINMAX or windows.h's max()/min() macros eat std::max below -- MSVC
// caught it as "illegal token on right side of '::'", which names neither
// the macro nor the header.
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shlobj.h>
#else
#include <dlfcn.h>
#endif
#include <fstream>
#include <set>
#include <system_error>
Expand Down Expand Up @@ -133,6 +144,22 @@ std::vector<std::unique_ptr<ASTNode>> getASTFromString(const std::string& code,

namespace {

// A "not found" that says where we actually looked. The old message named
// only the includer, so "searched relative to X" read as "only X was
// searched" -- which is how #503 came to be filed against a search that
// does cover the libraries folder.
std::string notFound(const char* noun, const std::string& filename, const std::string& currentFile) {
std::string msg = std::string(noun) + " '" + filename + "' not found. Searched:";
for (const auto& d : librarySearchDirs(currentFile)) {
msg += "\n " + d;
}
return msg;
}

} // namespace

namespace {

std::string readFile(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
Expand Down Expand Up @@ -163,8 +190,7 @@ std::vector<std::unique_ptr<ASTNode>> resolveIncludes(std::vector<std::unique_pt
const std::string& filename = inc.filepath->val;
auto libFile = findLibraryFile(currentFile, filename);
if (!libFile) {
throw std::runtime_error("Included file '" + filename + "' not found. Searched relative to: " +
(currentFile.empty() ? "current directory" : currentFile));
throw std::runtime_error(notFound("Included file", filename, currentFile));
}
std::string absLib = fs::absolute(*libFile).string();
if (visited.count(absLib) != 0) {
Expand All @@ -185,40 +211,98 @@ std::vector<std::unique_ptr<ASTNode>> resolveIncludes(std::vector<std::unique_pt

} // namespace

std::optional<std::string> findLibraryFile(const std::string& currFile, const std::string& libFile) {
std::vector<fs::path> dirs;
if (!currFile.empty()) {
dirs.push_back(fs::absolute(currFile).parent_path());
}
namespace {

char pathsep = ':';
std::string dfltPath;
const char* home = std::getenv("HOME");
// The user's own libraries folder -- OpenSCAD's
// PlatformUtils::userLibraryPath(), and the one an installer or a git clone
// of BOSL2 lands in.
std::string userLibraryDir() {
#if defined(_WIN32)
pathsep = ';';
// ASK Windows where My Documents is rather than assuming
// %USERPROFILE%\Documents. OneDrive's Known Folder Move -- on by default
// on a new machine -- relocates it to %USERPROFILE%\OneDrive\Documents,
// and a library OpenSCAD itself installed then sat somewhere we never
// looked (BelfrySCAD #503). Same call and same flag OpenSCAD makes
// (PlatformUtils-win.cc getFolderPath).
wchar_t buf[MAX_PATH] = {0};
if (SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, buf) == S_OK) {
return (fs::path(buf) / "OpenSCAD" / "libraries").string();
}
const char* userProfile = std::getenv("USERPROFILE");
if (userProfile) {
dfltPath = std::string(userProfile) + "\\Documents\\OpenSCAD\\libraries";
return userProfile ? (fs::path(userProfile) / "Documents" / "OpenSCAD" / "libraries").string()
: std::string();
#else
const char* home = std::getenv("HOME");
if (!home) {
return {};
}
#elif defined(__APPLE__)
if (home) {
dfltPath = std::string(home) + "/Documents/OpenSCAD/libraries";
#if defined(__APPLE__)
return (fs::path(home) / "Documents" / "OpenSCAD" / "libraries").string();
#else
return (fs::path(home) / ".local" / "share" / "OpenSCAD" / "libraries").string();
#endif
#endif
}

// Libraries shipped alongside this build -- OpenSCAD's
// PlatformUtils::resourcePath("libraries"). "Alongside" means beside the
// binary this code was linked into: the CLI executable, or the Python
// extension inside its installed package.
// ponytail: one candidate directory, no ../share/openscad/libraries walk --
// add the walk if a packaging layout ever puts the libraries somewhere
// other than next to the binary.
std::string bundledLibraryDir() {
fs::path self;
#if defined(_WIN32)
HMODULE mod = nullptr;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
| GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&userLibraryDir), &mod)) {
wchar_t buf[MAX_PATH] = {0};
if (GetModuleFileNameW(mod, buf, MAX_PATH)) {
self = buf;
}
}
#else
if (home) {
dfltPath = std::string(home) + "/.local/share/OpenSCAD/libraries";
Dl_info info;
if (dladdr(reinterpret_cast<void*>(&userLibraryDir), &info) && info.dli_fname) {
self = info.dli_fname;
}
#endif
if (self.empty()) {
return {};
}
std::error_code ec;
return (fs::absolute(self, ec).parent_path() / "libraries").string();
}

} // namespace

std::vector<std::string> librarySearchDirs(const std::string& currFile) {
std::vector<std::string> dirs;
if (!currFile.empty()) {
dirs.push_back(fs::absolute(currFile).parent_path().string());
}

#if defined(_WIN32)
const char pathsep = ';';
#else
const char pathsep = ':';
#endif

// OPENSCADPATH comes FIRST and adds to the built-in paths rather than
// replacing them -- exactly what OpenSCAD's parser_init() does. It used
// to replace them, so setting the variable for one library hid every
// other, including a BOSL2 sitting in the default folder (#503).
const char* envPath = std::getenv("OPENSCADPATH");
std::string env = envPath ? std::string(envPath) : dfltPath;
if (!env.empty()) {
if (envPath) {
std::string env(envPath);
size_t start = 0;
while (start <= env.size()) {
size_t pos = env.find(pathsep, start);
std::string part = (pos == std::string::npos) ? env.substr(start) : env.substr(start, pos - start);
if (!part.empty()) {
dirs.emplace_back(part);
dirs.push_back(part);
}
if (pos == std::string::npos) {
break;
Expand All @@ -227,8 +311,17 @@ std::optional<std::string> findLibraryFile(const std::string& currFile, const st
}
}

for (const auto& d : dirs) {
fs::path candidate = d / libFile;
for (const std::string& d : {userLibraryDir(), bundledLibraryDir()}) {
if (!d.empty()) {
dirs.push_back(d);
}
}
return dirs;
}

std::optional<std::string> findLibraryFile(const std::string& currFile, const std::string& libFile) {
for (const auto& d : librarySearchDirs(currFile)) {
fs::path candidate = fs::path(d) / libFile;
std::error_code ec;
if (fs::is_regular_file(candidate, ec)) {
return candidate.string();
Expand All @@ -255,9 +348,7 @@ LibraryFileResult getASTFromLibraryFile(const std::string& currFile, const std::
bool processIncludes) {
auto found = findLibraryFile(currFile, libFile);
if (!found) {
throw std::runtime_error("Library file '" + libFile +
"' not found in search paths. Searched in: current file directory, OPENSCADPATH, and "
"platform default paths.");
throw std::runtime_error(notFound("Library file", libFile, currFile));
}
auto ast = getASTFromFile(*found, includeComments, processIncludes);
return LibraryFileResult{std::move(ast), *found};
Expand Down Expand Up @@ -346,8 +437,7 @@ void collectProgram(const FileAst& nodes, const std::string& currentFile, bool i
const std::string& filename = inc.filepath->val;
auto libFile = findLibraryFile(currentFile, filename);
if (!libFile) {
throw std::runtime_error("Included file '" + filename + "' not found. Searched relative to: " +
(currentFile.empty() ? "current directory" : currentFile));
throw std::runtime_error(notFound("Included file", filename, currentFile));
}
std::string absLib = fs::absolute(*libFile).string();
if (!visited.insert(absLib).second) continue;
Expand Down
86 changes: 86 additions & 0 deletions tests/test_comments_and_files.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <gtest/gtest.h>

#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <random>
Expand Down Expand Up @@ -138,3 +140,87 @@ TEST(FileApi, FindLibraryFileReturnsNulloptWhenMissing) {
auto found = findLibraryFile("", "definitely_missing_file_xyz.scad");
EXPECT_FALSE(found.has_value());
}

namespace {

// RAII OPENSCADPATH, so one test's setting cannot leak into the next.
class ScopedOpenscadPath {
public:
explicit ScopedOpenscadPath(const std::string& value) {
const char* prev = std::getenv("OPENSCADPATH");
had_ = prev != nullptr;
if (had_) prev_ = prev;
set(value.c_str());
}
~ScopedOpenscadPath() {
if (had_) {
set(prev_.c_str());
} else {
#if defined(_WIN32)
_putenv_s("OPENSCADPATH", "");
#else
unsetenv("OPENSCADPATH");
#endif
}
}
private:
static void set(const char* v) {
#if defined(_WIN32)
_putenv_s("OPENSCADPATH", v);
#else
setenv("OPENSCADPATH", v, 1);
#endif
}
bool had_ = false;
std::string prev_;
};

} // namespace

TEST(FileApi, OpenscadPathAddsToTheBuiltInDirsRatherThanReplacingThem) {
// BelfrySCAD #503: `env = envPath ? envPath : dfltPath` meant setting
// OPENSCADPATH for one library hid every library in the default folder.
std::vector<std::string> without = librarySearchDirs("");
TempDir dir;
ScopedOpenscadPath scoped(dir.path().string());
std::vector<std::string> with = librarySearchDirs("");

EXPECT_EQ(with.size(), without.size() + 1);
EXPECT_EQ(with.front(), dir.path().string()); // and it is searched FIRST
for (const auto& d : without) {
EXPECT_NE(std::find(with.begin(), with.end(), d), with.end()) << d;
}
}

TEST(FileApi, FindLibraryFileSearchesOpenscadPath) {
TempDir libs;
fs::create_directories(libs.path() / "MYLIB");
fs::path libFile = libs.path() / "MYLIB" / "std.scad";
writeFile(libFile, "x = 1;\n");

TempDir docs;
fs::path mainFile = docs.path() / "main.scad";
writeFile(mainFile, "x = 1;\n");

ScopedOpenscadPath scoped(libs.path().string());
auto found = findLibraryFile(mainFile.string(), "MYLIB/std.scad");
ASSERT_TRUE(found.has_value());
EXPECT_EQ(fs::path(*found), libFile);
}

TEST(FileApi, MissingIncludeErrorNamesEveryDirectorySearched) {
TempDir dir;
fs::path mainFile = dir.path() / "main.scad";
writeFile(mainFile, "include <NOPE/std.scad>\n");

try {
getASTFromFile(mainFile.string());
FAIL() << "expected a missing-include error";
} catch (const std::runtime_error& e) {
std::string msg = e.what();
EXPECT_NE(msg.find("NOPE/std.scad"), std::string::npos) << msg;
for (const auto& d : librarySearchDirs(mainFile.string())) {
EXPECT_NE(msg.find(d), std::string::npos) << msg;
}
}
}