From ca86bf2ae8a71657c8d49117292515133bfe13a3 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 19 Jun 2026 23:21:04 +0300 Subject: [PATCH] builder: fix resolving symlinked modules nested in a .vmodules namespace (#27391) (#27502) --- vlib/v/builder/builder.v | 29 ++++++++- ...vmodules_package_compile_regression_test.v | 63 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index 724334026..d407992a8 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -846,11 +846,21 @@ fn (b &Builder) candidate_belongs_to_foreign_project(candidate_path string, impo if candidate_vmod_matches_import(abs_candidate, mod) { return false } + // A module installed as a symlink inside a lookup path (e.g. + // `.vmodules/einar_hjortdal/luuid -> /real/luuid`) resolves via + // os.real_path to a location outside the lookup path. Compare the + // logical (unresolved) path too, so such modules are still recognized + // as belonging to the project (see #27391). + candidate_lookup_path := comparable_path(candidate_path) for lookup in b.pref.lookup_path { abs_lookup := comparable_real_path(lookup) if path_is_at_or_inside(abs_candidate, abs_lookup) { return false } + lookup_path := comparable_path(lookup) + if path_is_at_or_inside(candidate_lookup_path, lookup_path) { + return false + } } return true } @@ -866,8 +876,25 @@ fn (b &Builder) path_belongs_to_lookup_path(path string) bool { return false } +// comparable_real_path normalizes a path for comparison, resolving symlinks +// via os.real_path. Use when both sides of a comparison should refer to the +// same physical location on disk. fn comparable_real_path(path string) string { - mut normalized := os.real_path(path).replace('\\', '/') + return comparable_path_from(os.real_path(path)) +} + +// comparable_path normalizes a path for comparison without resolving symlinks. +// Use when the original (logical) path matters, e.g. for symlinked modules +// inside `.vmodules` that should match their lookup path as-is. +fn comparable_path(path string) string { + return comparable_path_from(os.abs_path(path)) +} + +// comparable_path_from normalizes a path string for consistent comparison: +// converts backslashes to forward slashes, collapses duplicate separators, +// and strips a trailing slash. +fn comparable_path_from(path string) string { + mut normalized := path.replace('\\', '/') for normalized.contains('//') { normalized = normalized.replace('//', '/') } diff --git a/vlib/v/tests/vmodules_package_compile_regression_test.v b/vlib/v/tests/vmodules_package_compile_regression_test.v index e8dcbd1d8..990b01df5 100644 --- a/vlib/v/tests/vmodules_package_compile_regression_test.v +++ b/vlib/v/tests/vmodules_package_compile_regression_test.v @@ -75,3 +75,66 @@ fn test_issue_20147_vmodules_package_tests_compile() { res := os.execute('${os.quoted_path(issue_20147_vexe)} test .') assert res.exit_code == 0, res.output } + +// Regression test for https://github.com/vlang/v/issues/27391 : +// modules installed as symlinks inside a `.vmodules` namespace folder +// (e.g. `.vmodules/einar_hjortdal/luuid -> /real/luuid`) were resolved to +// their real path via os.real_path and then rejected as belonging to a +// different v.mod project, so `import einar_hjortdal.luuid` could not be found. +fn issue_27391_workspace() string { + return os.join_path(os.vtmp_dir(), 'issue_27391_symlinked_vmodules') +} + +fn issue_27391_write_project() ! { + workspace := issue_27391_workspace() + vmodules_ns := os.join_path(workspace, '.vmodules', 'einar_hjortdal') + real_luuid := os.join_path(workspace, 'real', 'luuid') + real_firebird := os.join_path(workspace, 'real', 'firebird') + app_root := os.join_path(workspace, 'app') + luuid_vmod := ['Module {', "\tname: 'luuid'", '}'].join_lines() + '\n' + luuid_contents := + ['module luuid', '', "pub fn hello() string { return 'luuid-ok' }"].join_lines() + '\n' + firebird_vmod := ['Module {', "\tname: 'firebird'", '}'].join_lines() + '\n' + firebird_contents := + ['module firebird', '', 'import einar_hjortdal.luuid', '', 'pub fn run() string { return luuid.hello() }'].join_lines() + + '\n' + app_vmod := ['Module {', "\tname: 'app'", '}'].join_lines() + '\n' + app_contents := + ['module main', '', 'import einar_hjortdal.luuid', 'import einar_hjortdal.firebird', '', 'fn main() {', '\tprintln(luuid.hello())', '\tprintln(firebird.run())', '}'].join_lines() + + '\n' + os.rmdir_all(workspace) or {} + os.mkdir_all(vmodules_ns)! + os.mkdir_all(real_luuid)! + os.mkdir_all(real_firebird)! + os.mkdir_all(app_root)! + issue_20147_write_file(os.join_path(real_luuid, 'v.mod'), luuid_vmod) + issue_20147_write_file(os.join_path(real_luuid, 'luuid.v'), luuid_contents) + issue_20147_write_file(os.join_path(real_firebird, 'v.mod'), firebird_vmod) + issue_20147_write_file(os.join_path(real_firebird, 'firebird.v'), firebird_contents) + // install both real modules as symlinks inside the `einar_hjortdal` namespace + os.symlink(real_luuid, os.join_path(vmodules_ns, 'luuid'))! + os.symlink(real_firebird, os.join_path(vmodules_ns, 'firebird'))! + issue_20147_write_file(os.join_path(app_root, 'v.mod'), app_vmod) + issue_20147_write_file(os.join_path(app_root, 'main.v'), app_contents) +} + +fn test_issue_27391_symlinked_namespaced_vmodules_import_compiles() { + issue_27391_write_project() or { + $if windows { + eprintln('skipping symlinked vmodules import regression test: ${err}') + return + } $else { + panic(err) + } + } + old_vmodules, had_vmodules := issue_20147_env_snapshot('VMODULES') + os.setenv('VMODULES', os.join_path(issue_27391_workspace(), '.vmodules'), true) + defer { + issue_20147_restore_env('VMODULES', old_vmodules, had_vmodules) + os.rmdir_all(issue_27391_workspace()) or {} + } + main_file := os.join_path(issue_27391_workspace(), 'app', 'main.v') + res := os.execute('${os.quoted_path(issue_20147_vexe)} run ${os.quoted_path(main_file)}') + assert res.exit_code == 0, res.output + assert res.output.trim_space() == 'luuid-ok\nluuid-ok', res.output +} -- 2.39.5