diff --git a/utils/librarypath/dproj_util.go b/utils/librarypath/dproj_util.go index 1ead4b0..6223b28 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 0000000..e4e2f16 --- /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 5429e42..8408e5c 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 {