From 2f8d4089c157c462c43b0a0747f5871c0bdac42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Baggio?= Date: Sat, 8 Aug 2026 20:34:26 -0300 Subject: [PATCH] fix(librarypath): resolve a subdirectory project's paths against its own directory A project that does not sit next to boss.json -- a test project under tests/, for example -- received dependency paths relative to the boss.json root instead of to its own folder. The paths written into DCC_UnitSearchPath then pointed nowhere and the compiler could not find the units, failing with "F2613 Unit '' not found" even though boss reported a successful install. rootPath came from filepath.Join(env.GetCurrentDir(), path.Dir(dprojName)), and dprojName is always an absolute, OS-native path because GetProjectNames builds every entry with filepath.Join. path.Dir only understands "/", so the two platforms lost the project's directory by different routes: windows: no "/" in a backslash path, so path.Dir answers ".", the Join collapses back to the working directory, and the os.Stat guard never fires because that directory does exist posix: path.Dir answers correctly, but joining an absolute result onto the working directory doubles the path, so os.Stat fails and the guard silently falls back to the working directory Both ended at the boss.json root. filepath.Dir answers correctly on either platform, so rootPath is now taken straight from it, shared by the .dproj writer and the Windows browsing path writer through one helper. The os.Stat fallback went with it: updateLibraryPathProject already stats dprojName and returns when it is missing, and a file that exists always has a parent that exists, so the branch could no longer be reached. Only projects outside the boss.json directory change behaviour. A project sitting at the root resolves to the same directory it always did. Co-Authored-By: Claude Opus 5 --- utils/librarypath/dproj_util.go | 19 ++++-- utils/librarypath/dproj_util_test.go | 92 ++++++++++++++++++++++++++++ utils/librarypath/global_util_win.go | 5 +- 3 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 utils/librarypath/dproj_util_test.go diff --git a/utils/librarypath/dproj_util.go b/utils/librarypath/dproj_util.go index 1ead4b07..6223b28c 100644 --- a/utils/librarypath/dproj_util.go +++ b/utils/librarypath/dproj_util.go @@ -4,7 +4,6 @@ package librarypath import ( "os" - "path" "path/filepath" "regexp" "strings" @@ -128,6 +127,18 @@ func updateGlobalBrowsingPath(pkg *domain.Package) { } } +// dprojRootPath returns the directory a project file's own paths are relative to. +// dprojName is always an absolute, OS-native path (GetProjectNames builds every entry +// with filepath.Join), so its parent has to be resolved with filepath.Dir. path.Dir +// only understands "/": on Windows it finds no separator in a backslash path and +// answers ".", and on POSIX it answers correctly but the caller then joined that +// absolute result onto the working directory, doubling the path. Both ways the +// project's real directory was lost. Kept in this file, rather than inlined, so the +// Windows-only browsing path caller shares one definition with the .dproj caller. +func dprojRootPath(dprojName string) string { + return filepath.Dir(dprojName) +} + // updateLibraryPathProject updates the library path in the project file. func updateLibraryPathProject(dprojName string) { doc := etree.NewDocument() @@ -151,11 +162,7 @@ func updateLibraryPathProject(dprojName string) { if child == nil { child = createTagLibraryPath(children) } - rootPath := filepath.Join(env.GetCurrentDir(), path.Dir(dprojName)) - if _, err = os.Stat(rootPath); os.IsNotExist(err) { - rootPath = env.GetCurrentDir() - } - processCurrentPath(child, rootPath) + processCurrentPath(child, dprojRootPath(dprojName)) } } diff --git a/utils/librarypath/dproj_util_test.go b/utils/librarypath/dproj_util_test.go new file mode 100644 index 00000000..e4e2f16c --- /dev/null +++ b/utils/librarypath/dproj_util_test.go @@ -0,0 +1,92 @@ +//nolint:testpackage // Testing internal dproj utility functions +package librarypath + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/beevik/etree" + "github.com/hashload/boss/internal/adapters/secondary/filesystem" + "github.com/hashload/boss/internal/adapters/secondary/repository" + "github.com/hashload/boss/internal/core/services/packages" + "github.com/hashload/boss/pkg/pkgmanager" +) + +const mockDprojContent = ` + + + $(DCC_UnitSearchPath) + + +` + +// TestUpdateLibraryPathProject_SubdirectoryProject verifies that a .dproj located in a +// subdirectory of the boss.json root gets paths relative to its OWN directory, not to +// the root. dprojName is always an absolute, OS-native path (built via filepath.Join), +// so resolving its parent directory must use filepath.Dir rather than path.Dir -- the +// latter only understands "/" and silently collapses to "." on a Windows backslash path. +func TestUpdateLibraryPathProject_SubdirectoryProject(t *testing.T) { + tempDir := t.TempDir() + t.Chdir(tempDir) + + fs := filesystem.NewOSFileSystem() + packageRepo := repository.NewFilePackageRepository(fs) + lockRepo := repository.NewFileLockRepository(fs) + packageService := packages.NewPackageService(packageRepo, lockRepo) + pkgmanager.SetInstance(packageService) + + depSrcDir := filepath.Join(tempDir, "modules", "mydep", "src") + if err := os.MkdirAll(depSrcDir, 0755); err != nil { + t.Fatalf("Failed to create dependency src dir: %v", err) + } + if err := os.WriteFile(filepath.Join(depSrcDir, "dummy.pas"), []byte("unit dummy;"), 0600); err != nil { + t.Fatalf("Failed to write dummy.pas: %v", err) + } + depBossJSON := `{"name": "mydep", "mainsrc": "src"}` + depBossJSONPath := filepath.Join(tempDir, "modules", "mydep", "boss.json") + if err := os.WriteFile(depBossJSONPath, []byte(depBossJSON), 0600); err != nil { + t.Fatalf("Failed to write dependency boss.json: %v", err) + } + + projectDir := filepath.Join(tempDir, "app") + if err := os.MkdirAll(projectDir, 0755); err != nil { + t.Fatalf("Failed to create project dir: %v", err) + } + dprojPath := filepath.Join(projectDir, "project.dproj") + if err := os.WriteFile(dprojPath, []byte(mockDprojContent), 0600); err != nil { + t.Fatalf("Failed to write mock dproj: %v", err) + } + + updateLibraryPathProject(dprojPath) + + doc := etree.NewDocument() + if err := doc.ReadFromFile(dprojPath); err != nil { + t.Fatalf("Failed to read updated dproj: %v", err) + } + + var searchPath string + for _, group := range doc.Root().FindElements("PropertyGroup") { + if el := group.SelectElement("DCC_UnitSearchPath"); el != nil { + searchPath = el.Text() + } + } + if searchPath == "" { + t.Fatal("DCC_UnitSearchPath not found in updated dproj") + } + + expected := filepath.Join("..", "modules", "mydep", "src") + if !strings.Contains(filepath.Clean(searchPath), expected) { + t.Errorf("expected DCC_UnitSearchPath to contain %q (relative to the project's own directory), got %q", + expected, searchPath) + } + + wrong := filepath.Join("modules", "mydep", "src") + for _, entry := range strings.Split(searchPath, ";") { + if filepath.Clean(entry) == wrong { + t.Errorf("DCC_UnitSearchPath contains %q, which is relative to the boss.json root instead of "+ + "the project's own directory -- rootPath was computed wrong", entry) + } + } +} diff --git a/utils/librarypath/global_util_win.go b/utils/librarypath/global_util_win.go index 5429e420..8408e5cb 100644 --- a/utils/librarypath/global_util_win.go +++ b/utils/librarypath/global_util_win.go @@ -5,8 +5,6 @@ package librarypath import ( - "path" - "path/filepath" "strings" "github.com/hashload/boss/pkg/consts" @@ -103,8 +101,7 @@ func updateGlobalBrowsingByProject(dprojName string, setReadOnly bool) { } splitPaths := strings.Split(paths, ";") - rootPath := filepath.Join(env.GetCurrentDir(), path.Dir(dprojName)) - newSplitPaths := GetNewBrowsingPaths(splitPaths, false, rootPath, setReadOnly) + newSplitPaths := GetNewBrowsingPaths(splitPaths, false, dprojRootPath(dprojName), setReadOnly) newPaths := strings.Join(newSplitPaths, ";") err = delphiPlatform.SetStringValue(BrowsingPathRegistry, newPaths) if err != nil {