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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ option(DAS_AUDIO_DISABLED "Disable dasAudio (Miniaudio sound library)" OFF)
option(DAS_STDDLG_DISABLED "Disable dasStdDlg (File new,open,save etc dialogs)" OFF)
option(DAS_STBIMAGE_DISABLED "Disable dasStbImage (StbImage bindings, image loading and saving)" OFF)

option(DAS_PUGIXML_DISABLED "Disable dasPUGIXML (xml parsing library)" ON)
option(DAS_PUGIXML_DISABLED "Disable dasPUGIXML (xml parsing library)" OFF)
option(DAS_SQLITE_DISABLED "Disable dasSQLITE (sqlite3 library)" ON)
option(DAS_TOOLS_DISABLED "Disable dasTools" OFF)
option(DAS_TREE_SITTER_DISABLED "Disable tree-sitter-daslang grammar (no dependencies)" OFF)
Expand Down
5 changes: 5 additions & 0 deletions daslib/daspkg.das
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ struct ReleaseSpec {
include_globs : array<string> //!< asset globs to ship
exclude_globs : array<string> //!< asset globs to skip
forced_modules : array<string> //!< force-include shared module by das_name (auto-detection misses media-only or runtime-loaded packages)
bundle_id : string //!< macOS .app CFBundleIdentifier (reverse-DNS); defaults to com.<author>.<name>
}

var _release_spec : ReleaseSpec
Expand All @@ -163,3 +164,7 @@ def release_shared_module(name : string) {
_release_spec.forced_modules |> push(name)
}

def release_bundle_id(id : string) {
_release_spec.bundle_id = id
}

5 changes: 3 additions & 2 deletions daslib/module_path.das
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ module module_path shared public
//! ``<rel>`` is the substring of the call-site directory starting at
//! the last ``/modules/`` segment (e.g. ``modules/dasCards/cards``).
//! Tiers 1+2 are skipped when the call site has no ``/modules/`` segment
//! (project-local code outside the package layout); tier 3 is always
//! returned, so the result is never empty.
//! (project-local code outside the package layout); tier 3 then returns
//! the baked dir if it still exists on disk (dev) or ``<exe_dir>`` if it
//! doesn't (relocated bundle). The result is never empty.
//!
//! Usage::
//!
Expand Down
1 change: 1 addition & 0 deletions include/daScript/simulate/aot_builtin.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace das {
DAS_API uint64_t get_context_share_counter ( Context * context );

DAS_API char * builtin_das_root ( Context * context, LineInfoArg * at );
DAS_API char * builtin_resolve_this_module_dir ( const char * baked_path, Context * context );
DAS_API char * builtin_get_das_version ( Context * context, LineInfoArg * at );
DAS_API void builtin_throw ( char * text, Context * context, LineInfoArg * at );
DAS_API void builtin_print ( char * text, Context * context, LineInfoArg * at );
Expand Down
7 changes: 4 additions & 3 deletions modules/dasLLVM/daslib/llvm_exe.das
Original file line number Diff line number Diff line change
Expand Up @@ -1028,9 +1028,10 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef;
}

let main_ret = LLVMBuildCall2(builder, g_fn_types[start_fn_name], LLVMGetNamedFunction(g_mod, start_fn_name), main_params, "")
// Drain debug agents, modules, AOT library, env — mirrors what the interpreter
// driver does at shutdown. Without this the static g_DebugAgents map dtor
// races ref_count_mutex during __cxa_finalize_ranges (issue #2583).
// Drain debug agents, modules, AOT library, env — same teardown the interpreter
// does, minus the handle-leak dump (shipped binaries should be quiet by default).
// Without this the static g_DebugAgents map dtor races ref_count_mutex during
// __cxa_finalize_ranges (issue #2583).
let shutdown_fn_type = LLVMFunctionType(types.t_void, array<LLVMTypeRef>())
var shutdown_fn = LLVMGetNamedFunction(g_mod, "jit_shutdown")
if (shutdown_fn == null) {
Expand Down
2 changes: 1 addition & 1 deletion skills/daspkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ The macro captures the call-site source file path at expansion, then walks the s

`<rel>` is the suffix starting at the last `/modules/` segment.

**Project-local code (no `/modules/` in path) currently falls straight to tier 3** — `get_this_module_dir()` from user code like `main.das` returns the dev-time baked dir, which doesn't exist in a relocated bundle. Symptom: silent asset-load failures (e.g. fonts) when running the released bundle on a fresh machine, even though library code under `/modules/` works fine. Workaround: expose the asset dir via a `def public` getter from a library module under `/modules/`, then call it from user code. See [skills/filesystem.md](skills/filesystem.md) ("Finding bundled asset files at runtime") for the details.
**Project-local code (no `/modules/` in path)** skips tiers 1+2 and goes straight to tier 3, which returns the baked dir if it still exists (dev) or `<exe_dir>` if it doesn't (relocated bundle). So `path_join(get_this_module_dir(), "asset.png")` from a project-local `main.das` works in both dev and shipped bundles, as long as the asset is shipped sitting next to the exe.

**Call from inside a function — not a top-level `let` initializer.** daslang's `-exe` JIT path has a known limitation: top-level `let`s that call Context-allocating builtins (like `get_this_module_dir()`, `get_das_root()`, `dir_name(get_module_file_name(...))`) emit JIT-process-baked function pointers that are wrong under ASLR in the standalone exe. The same call works fine inside any function body. Tracked separately; once the daslang fix lands, the top-level form will Just Work.

Expand Down
5 changes: 1 addition & 4 deletions skills/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,7 @@ def load_assets() {

The macro captures the call-site source-file path at expansion, then a 3-tier resolver walks `<exe_dir>/<rel>` → `<das_root>/<rel>` → `dir_name(baked)`. `<rel>` is the suffix starting at the last `/modules/` segment.

**Caveat for project-local code (no `/modules/` in source path):** `get_this_module_dir()` from a user's `main.das` (or anywhere outside `/modules/...`) currently falls straight to tier 3 — i.e. returns the dev-time baked dir, which doesn't exist on a relocated bundle's machine. This makes `"{get_this_module_dir()}/<asset>"` from `main.das` resolve to a non-existent path post-release. Two workarounds until the resolver learns to fall back to `<exe_dir>` when baked is gone:

1. Have a sibling module under `/modules/` expose its dir as a public function (`def public my_module_dir() : string { return get_this_module_dir() }`) and call that from user code.
2. If the user app needs only the exe-relative root, plumb it via a builtin (none exists yet — `getExecutableFileName` is C++-only).
**Project-local code (no `/modules/` in source path)** — e.g. `get_this_module_dir()` called from a user's `main.das` — skips tiers 1+2 (no `<rel>`) and goes to tier 3, which returns `dir_name(baked)` if it still exists on disk (dev) or `<exe_dir>` if it doesn't (relocated bundle). Either way you get an existing directory. Ship project-local assets next to the exe and `path_join(get_this_module_dir(), "asset.png")` works in both dev and shipped bundles.

See [skills/daspkg.md](skills/daspkg.md#L224) for the bundle-shipping side of the same topic.

Expand Down
31 changes: 26 additions & 5 deletions src/builtin/module_builtin_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1542,13 +1542,20 @@ namespace das
// 3. dir_name(baked_path) — dev (interpreted from source tree)
// <rel> is the substring of dir_name(baked) starting at the last
// "/modules/" segment. Tiers 1+2 are skipped when baked has no /modules/
// segment (e.g. project-local code outside the package layout).
// segment (e.g. project-local code outside the package layout); when the
// baked dir has also gone missing on the target machine (relocated bundle),
// tier 3's fallback is <exe_dir> rather than the dead dev path.
char * builtin_resolve_this_module_dir ( const char * baked_path, Context * context ) {
namespace fs = std::filesystem;
if ( !baked_path || !*baked_path ) return context->allocateString("", nullptr);
// generic_string() (here and at every other path-to-string conversion
// in this function) emits forward-slash separators on every platform,
// matching getDasRoot() and the rest of daslang's path conventions.
// Without it Windows would return native backslashes that break string
// compares against `/`-formed paths from script-side daslang code.
fs::path baked(baked_path);
fs::path baked_dir = baked.parent_path();
std::string baked_dir_str = baked_dir.string();
std::string baked_dir_str = baked_dir.generic_string();
// Find the last "/modules/" boundary; everything from that segment
// onward is the bundle/SDK-relative suffix (e.g.
// "modules/das-cards/cards"). rfind, not find — for nested layouts
Expand All @@ -1572,17 +1579,31 @@ namespace das
fs::path candidate = exeDir / rel;
std::error_code ec;
if ( fs::is_directory(candidate, ec) ) {
return context->allocateString(candidate.string().c_str(), nullptr);
return context->allocateString(candidate.generic_string().c_str(), nullptr);
}
}
// Tier 2 — das_root
fs::path candidate = fs::path(das::getDasRoot().c_str()) / rel;
std::error_code ec;
if ( fs::is_directory(candidate, ec) ) {
return context->allocateString(candidate.string().c_str(), nullptr);
return context->allocateString(candidate.generic_string().c_str(), nullptr);
}
}
// Tier 3 — baked dir as fallback. Special case: project-local code
// (rel empty so tiers 1+2 were skipped) running from a relocated
// bundle where the dev-time baked dir no longer exists — fall back
// to <exe_dir> so assets shipped next to the exe are findable.
if ( rel.empty() ) {
std::error_code ec;
if ( !fs::is_directory(baked_dir, ec) ) {
das::string exeFile = das::getExecutableFileName();
if ( !exeFile.empty() ) {
fs::path exeDir = fs::path(exeFile.c_str()).parent_path();
if ( exeDir.empty() ) exeDir = ".";
return context->allocateString(exeDir.generic_string().c_str(), nullptr);
}
}
}
// Tier 3 — baked dir as fallback
return context->allocateString(baked_dir_str.c_str(), nullptr);
Comment thread
borisbat marked this conversation as resolved.
}

Expand Down
13 changes: 11 additions & 2 deletions src/builtin/module_jit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -798,13 +798,22 @@ extern "C" {
: fmt::format_to(cmd, FMT_STRING("\"\"{}\" \"{}\" \"{}\" \"{}\" msvcrt.lib -link {} -OUT:\"{}\" 2>&1\""), linker, objFilePath, runtimeLibrary, compilerLibrary, linkerParam, libraryName);
#elif defined(__APPLE__)
const auto linkerParam = isShared ? "-shared " : "";
const auto rpath = "-Wl,-rpath," + get_prefix(runtimeLibrary);
// @executable_path first → relocated bundle finds dylibs next to the exe;
// build-tree path second → dev workflow keeps working without copying dylibs.
// The embedded `\" \"` splits the format-string's outer quotes so the linker
// sees two distinct -Wl,-rpath flags, not one with an embedded space.
const auto rpath = "-Wl,-rpath,@executable_path\" \"-Wl,-rpath," + get_prefix(runtimeLibrary);
auto result = compilerLibrary.empty()
? fmt::format_to(cmd, FMT_STRING("\"{}\" {} \"{}\" -o \"{}\" \"{}\" \"{}\" 2>&1"), linker, linkerParam, rpath, libraryName, runtimeLibrary, objFilePath)
: fmt::format_to(cmd, FMT_STRING("\"{}\" {} \"{}\" -o \"{}\" \"{}\" \"{}\" \"{}\" 2>&1"), linker, linkerParam, rpath, libraryName, runtimeLibrary, compilerLibrary, objFilePath);
#else
const auto linkerParam = isShared ? "-shared" : "";
const auto rpath = "-Wl,-rpath," + get_prefix(runtimeLibrary);
// $ORIGIN first → relocated bundle finds .so next to the exe;
// build-tree path second → dev workflow keeps working without copying.
// \$ escapes the dollar so popen's shell passes $ORIGIN to ld unexpanded.
// The embedded `\" \"` splits the format-string's outer quotes so the linker
// sees two distinct -Wl,-rpath flags, not one with an embedded space.
const auto rpath = "-Wl,-rpath,\\$ORIGIN\" \"-Wl,-rpath," + get_prefix(runtimeLibrary);
auto result = compilerLibrary.empty()
? fmt::format_to(cmd, FMT_STRING("\"{}\" {} \"{}\" -o \"{}\" \"{}\" \"{}\" 2>&1"),
linker, linkerParam, rpath, libraryName, objFilePath, runtimeLibrary)
Expand Down
48 changes: 48 additions & 0 deletions tests/fio/get_this_module_dir_resolver.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
options gen2
require dastest/testing_boost public

require daslib/fio
require daslib/module_path

[test]
def test_real_project_local_path_returns_baked_dir(t : T?) {
t |> run("real project-local path returns its dir_name") @(t : T?) {
// This test file is project-local (sits in tests/fio/, no /modules/ segment).
let real_baked = "{get_das_root()}/tests/fio/get_this_module_dir_resolver.das"
let dir = __builtin_resolve_this_module_dir(real_baked)
t |> equal(dir, "{get_das_root()}/tests/fio")
}
Comment thread
borisbat marked this conversation as resolved.
}

[test]
def test_nonexistent_project_local_falls_back_to_exe_dir(t : T?) {
t |> run("nonexistent project-local baked path falls back to an existing dir") @(t : T?) {
// No /modules/ segment AND dir doesn't exist on disk → should fall
// back to <exe_dir> rather than returning the dead path. Models the
// relocated-bundle scenario where main.das's dev-time dir is gone.
let dead = "/__no_such_dir_42__/__no_such_subdir__/main.das"
let dir = __builtin_resolve_this_module_dir(dead)
t |> success(dir != "/__no_such_dir_42__/__no_such_subdir__",
"must not return the dead baked dir, got '{dir}'")
t |> success(fexist(dir), "fallback dir '{dir}' must exist on disk")
}
}

[test]
def test_nonexistent_modules_path_keeps_baked(t : T?) {
t |> run("nonexistent /modules/ baked path returns baked dir unchanged") @(t : T?) {
// When rel is non-empty the new fallback does NOT fire — preserves
// existing tier-3 behavior for module paths so we don't accidentally
// mask a missing module with the exe dir.
let dead = "/__no_such_root__/modules/__no_such_module__/foo.das"
let dir = __builtin_resolve_this_module_dir(dead)
t |> equal(dir, "/__no_such_root__/modules/__no_such_module__")
}
}

[test]
def test_empty_baked_returns_empty(t : T?) {
t |> run("empty input returns empty") @(t : T?) {
t |> equal(__builtin_resolve_this_module_dir(""), "")
}
}
Loading
Loading