diff --git a/internal/adapters/secondary/repository/repository_test.go b/internal/adapters/secondary/repository/repository_test.go
index 813a305b..6098cec2 100644
--- a/internal/adapters/secondary/repository/repository_test.go
+++ b/internal/adapters/secondary/repository/repository_test.go
@@ -6,6 +6,7 @@ import (
"errors"
"io"
"os"
+ "path/filepath"
"testing"
"time"
@@ -27,6 +28,7 @@ func NewMockFileSystem() *MockFileSystem {
}
func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
+ name = filepath.ToSlash(name)
if data, ok := m.files[name]; ok {
return data, nil
}
@@ -34,6 +36,7 @@ func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
}
func (m *MockFileSystem) WriteFile(name string, data []byte, _ os.FileMode) error {
+ name = filepath.ToSlash(name)
m.files[name] = data
return nil
}
@@ -43,6 +46,7 @@ func (m *MockFileSystem) MkdirAll(_ string, _ os.FileMode) error {
}
func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
+ name = filepath.ToSlash(name)
if _, ok := m.files[name]; ok {
//nolint:nilnil // Mock for testing
return nil, nil
@@ -51,6 +55,7 @@ func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
}
func (m *MockFileSystem) Remove(name string) error {
+ name = filepath.ToSlash(name)
delete(m.files, name)
return nil
}
@@ -60,6 +65,8 @@ func (m *MockFileSystem) RemoveAll(_ string) error {
}
func (m *MockFileSystem) Rename(oldpath, newpath string) error {
+ oldpath = filepath.ToSlash(oldpath)
+ newpath = filepath.ToSlash(newpath)
if data, ok := m.files[oldpath]; ok {
m.files[newpath] = data
delete(m.files, oldpath)
@@ -82,6 +89,7 @@ func (m *MockFileSystem) Create(_ string) (io.WriteCloser, error) {
}
func (m *MockFileSystem) Exists(name string) bool {
+ name = filepath.ToSlash(name)
_, ok := m.files[name]
return ok
}
diff --git a/internal/core/services/compiler/executor.go b/internal/core/services/compiler/executor.go
index b9b5c3f9..e86f027e 100644
--- a/internal/core/services/compiler/executor.go
+++ b/internal/core/services/compiler/executor.go
@@ -55,8 +55,60 @@ func buildSearchPath(dep *domain.Dependency) string {
return searchPath
}
+func compileLazarus(lazarusPath string, tracker *BuildTracker) bool {
+ if tracker == nil || !tracker.IsEnabled() {
+ msg.Info(" 🔨 Building Lazarus project/package: " + filepath.Base(lazarusPath))
+ }
+
+ _, err := exec.LookPath("lazbuild")
+ if err != nil {
+ if tracker == nil || !tracker.IsEnabled() {
+ msg.Err(" ❌ 'lazbuild' compiler not found on PATH. Please install Lazarus/lazbuild to compile.")
+ }
+ return false
+ }
+
+ absPath, _ := filepath.Abs(lazarusPath)
+ absDir := filepath.Dir(absPath)
+
+ cmd := exec.Command("lazbuild", "--build-mode=Debug", absPath)
+ cmd.Dir = absDir
+
+ buildLog := filepath.Join(absDir, "build_boss_" + strings.TrimSuffix(filepath.Base(lazarusPath), filepath.Ext(lazarusPath)) + ".log")
+ logFile, err := os.OpenFile(buildLog, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
+ if err != nil {
+ if tracker == nil || !tracker.IsEnabled() {
+ msg.Warn(" ⚠️ Error creating build log file: %v", err)
+ }
+ return false
+ }
+ defer logFile.Close()
+
+ cmd.Stdout = logFile
+ cmd.Stderr = logFile
+
+ if err := cmd.Run(); err != nil {
+ if tracker == nil || !tracker.IsEnabled() {
+ msg.Err(" ❌ Failed to compile, see " + buildLog + " for more information: %v", err)
+ }
+ return false
+ }
+
+ if tracker == nil || !tracker.IsEnabled() {
+ msg.Info(" ✅️ Success!")
+ }
+
+ _ = os.Remove(buildLog)
+ return true
+}
+
//nolint:funlen,gocognit,lll // Complex compilation orchestration with long function signature
func compile(dprojPath string, dep *domain.Dependency, rootLock domain.PackageLock, tracker *BuildTracker, selectedCompiler *compilerselector.SelectedCompiler) bool {
+ ext := strings.ToLower(filepath.Ext(dprojPath))
+ if ext == ".lpi" || ext == ".lpk" {
+ return compileLazarus(dprojPath, tracker)
+ }
+
if tracker == nil || !tracker.IsEnabled() {
msg.Info(" 🔨 Building " + filepath.Base(dprojPath))
}
diff --git a/internal/core/services/installer/core.go b/internal/core/services/installer/core.go
index b22c080c..113a99c1 100644
--- a/internal/core/services/installer/core.go
+++ b/internal/core/services/installer/core.go
@@ -118,9 +118,11 @@ func DoInstall(config env.ConfigProvider, options InstallOptions, pkg *domain.Pa
msg.SetProgressTracker(nil)
progress.Stop()
- paths.EnsureCleanModulesDir(dependencies, pkg.Lock)
+ paths.EnsureCleanModulesDir(dependencies, pkg.Lock, len(options.Args) == 0)
- pkg.Lock.CleanRemoved(dependencies)
+ if len(options.Args) == 0 {
+ pkg.Lock.CleanRemoved(dependencies)
+ }
if err := pkgmanager.SavePackageCurrent(pkg); err != nil {
msg.Warn("⚠️ Failed to save package: %v", err)
}
diff --git a/internal/core/services/installer/core_test.go b/internal/core/services/installer/core_test.go
index bc71b67e..25f170d9 100644
--- a/internal/core/services/installer/core_test.go
+++ b/internal/core/services/installer/core_test.go
@@ -68,3 +68,27 @@ func TestAddWarning(t *testing.T) {
t.Errorf("Expected warning 'Test warning', got %q", ctx.warnings[0])
}
}
+
+func TestCollectDependenciesToInstall_WithFilter(t *testing.T) {
+ pkg := &domain.Package{
+ Dependencies: map[string]string{
+ "github.com/hashload/horse": "^1.0.0",
+ "github.com/hashload/boss": "^2.0.0",
+ },
+ }
+
+ // Test empty filter (should return all)
+ all := collectDependenciesToInstall(pkg, []string{})
+ if len(all) != 2 {
+ t.Errorf("Expected 2 dependencies, got %d", len(all))
+ }
+
+ // Test with filter (should return only matching)
+ filtered := collectDependenciesToInstall(pkg, []string{"horse"})
+ if len(filtered) != 1 {
+ t.Errorf("Expected 1 dependency, got %d", len(filtered))
+ }
+ if filtered[0].Repository != "github.com/hashload/horse" {
+ t.Errorf("Expected horse dependency, got %q", filtered[0].Repository)
+ }
+}
diff --git a/internal/core/services/paths/paths.go b/internal/core/services/paths/paths.go
index f717d8c7..9896f4bf 100644
--- a/internal/core/services/paths/paths.go
+++ b/internal/core/services/paths/paths.go
@@ -16,7 +16,7 @@ import (
// EnsureCleanModulesDir ensures that the modules directory is clean and contains only the required dependencies.
//
//nolint:gocognit // Refactoring would reduce readability
-func EnsureCleanModulesDir(dependencies []domain.Dependency, lock domain.PackageLock) {
+func EnsureCleanModulesDir(dependencies []domain.Dependency, lock domain.PackageLock, cleanAll bool) {
cacheDir := env.GetModulesDir()
cacheDirInfo, err := os.Stat(cacheDir)
if os.IsNotExist(err) {
@@ -47,7 +47,7 @@ func EnsureCleanModulesDir(dependencies []domain.Dependency, lock domain.Package
continue
}
- if !utils.Contains(dependenciesNames, info.Name()) {
+ if cleanAll && !utils.Contains(dependenciesNames, info.Name()) {
remove:
if err = os.RemoveAll(filepath.Join(cacheDir, info.Name())); err != nil {
msg.Warn("⚠️ Failed to remove old cache: %s", err.Error())
diff --git a/internal/core/services/paths/paths_test.go b/internal/core/services/paths/paths_test.go
index 514d6939..688bd27a 100644
--- a/internal/core/services/paths/paths_test.go
+++ b/internal/core/services/paths/paths_test.go
@@ -59,7 +59,7 @@ func TestEnsureCleanModulesDir_CreatesDir(t *testing.T) {
}
// EnsureCleanModulesDir should create the modules directory
- paths.EnsureCleanModulesDir(deps, lock)
+ paths.EnsureCleanModulesDir(deps, lock, true)
// Verify modules directory was created
modulesDir := filepath.Join(tempDir, consts.FolderDependencies)
@@ -114,7 +114,7 @@ func TestEnsureCleanModulesDir_RemovesOldDependencies(t *testing.T) {
}
// EnsureCleanModulesDir should remove old dependency
- paths.EnsureCleanModulesDir(deps, lock)
+ paths.EnsureCleanModulesDir(deps, lock, true)
// Verify old dependency was removed
if _, err := os.Stat(oldDepDir); !os.IsNotExist(err) {
@@ -126,3 +126,54 @@ func TestEnsureCleanModulesDir_RemovesOldDependencies(t *testing.T) {
t.Error("EnsureCleanModulesDir() should keep current dependency directories")
}
}
+
+func TestEnsureCleanModulesDir_KeepOldDependenciesOnSelective(t *testing.T) {
+ // Create a temp directory for workspace
+ tempDir := t.TempDir()
+
+ // Save original state and set not global
+ originalGlobal := env.GetGlobal()
+ defer env.SetGlobal(originalGlobal)
+ env.SetGlobal(false)
+
+ // Change to temp directory
+ t.Chdir(tempDir)
+
+ // Create modules directory
+ modulesDir := filepath.Join(tempDir, consts.FolderDependencies)
+ if err := os.MkdirAll(modulesDir, 0755); err != nil {
+ t.Fatalf("Failed to create modules dir: %v", err)
+ }
+
+ // Create an old dependency directory that should NOT be removed because cleanAll is false
+ oldDepDir := filepath.Join(modulesDir, "old-dependency")
+ if err := os.MkdirAll(oldDepDir, 0755); err != nil {
+ t.Fatalf("Failed to create old dependency dir: %v", err)
+ }
+
+ // Create a current dependency directory that should be kept
+ currentDepDir := filepath.Join(modulesDir, "horse")
+ if err := os.MkdirAll(currentDepDir, 0755); err != nil {
+ t.Fatalf("Failed to create current dependency dir: %v", err)
+ }
+
+ // Define current dependencies
+ dep := domain.ParseDependency("github.com/hashload/horse", "^1.0.0")
+ deps := []domain.Dependency{dep}
+ lock := domain.PackageLock{
+ Installed: map[string]domain.LockedDependency{},
+ }
+
+ // EnsureCleanModulesDir with cleanAll=false should keep the old dependency
+ paths.EnsureCleanModulesDir(deps, lock, false)
+
+ // Verify old dependency was NOT removed
+ if _, err := os.Stat(oldDepDir); os.IsNotExist(err) {
+ t.Error("EnsureCleanModulesDir() with cleanAll=false should NOT remove old dependency directories")
+ }
+
+ // Verify current dependency was kept
+ if _, err := os.Stat(currentDepDir); os.IsNotExist(err) {
+ t.Error("EnsureCleanModulesDir() should keep current dependency directories")
+ }
+}
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index 7555b91f..e366a367 100644
--- a/pkg/consts/consts.go
+++ b/pkg/consts/consts.go
@@ -13,6 +13,7 @@ const (
FileExtensionDpr = ".dpr"
FileExtensionDproj = ".dproj"
FileExtensionLpi = ".lpi"
+ FileExtensionLpk = ".lpk"
FilePackageLockOld = "boss.lock"
FolderDependencies = "modules"
diff --git a/utils/librarypath/dproj_util.go b/utils/librarypath/dproj_util.go
index c9006275..3abe108f 100644
--- a/utils/librarypath/dproj_util.go
+++ b/utils/librarypath/dproj_util.go
@@ -18,8 +18,8 @@ import (
var (
//nolint:lll // Regex pattern readability is important
- reProjectFile = regexp.MustCompile(`.*` + regexp.QuoteMeta(consts.FileExtensionDproj) + `|.*` + regexp.QuoteMeta(consts.FileExtensionLpi) + `$`)
- reLazarusFile = regexp.MustCompile(`.*` + regexp.QuoteMeta(consts.FileExtensionLpi) + `$`)
+ reProjectFile = regexp.MustCompile(`.*` + regexp.QuoteMeta(consts.FileExtensionDproj) + `|.*` + regexp.QuoteMeta(consts.FileExtensionLpi) + `|.*` + regexp.QuoteMeta(consts.FileExtensionLpk) + `$`)
+ reLazarusFile = regexp.MustCompile(`.*` + regexp.QuoteMeta(consts.FileExtensionLpi) + `|.*` + regexp.QuoteMeta(consts.FileExtensionLpk) + `$`)
)
// updateDprojLibraryPath updates the library path in the project file.
@@ -40,28 +40,41 @@ func updateOtherUnitFilesProject(lpiName string) {
doc := etree.NewDocument()
info, err := os.Stat(lpiName)
if os.IsNotExist(err) || info.IsDir() {
- msg.Err("❌ .lpi not found.")
+ msg.Err("❌ Lazarus project/package file not found.")
return
}
err = doc.ReadFromFile(lpiName)
if err != nil {
- msg.Err("❌ Error on read lpi: %s", err)
+ msg.Err("❌ Error on read lazarus file: %s", err)
return
}
root := doc.Root()
compilerOptions := root.SelectElement(consts.XMLTagNameCompilerOptions)
- processCompilerOptions(compilerOptions)
+ if compilerOptions != nil {
+ processCompilerOptions(compilerOptions)
+ }
projectOptions := root.SelectElement(consts.XMLTagNameProjectOptions)
+ if projectOptions != nil {
+ buildModes := projectOptions.SelectElement(consts.XMLTagNameBuildModes)
+ if buildModes != nil {
+ for _, item := range buildModes.SelectElements(consts.XMLTagNameItem) {
+ attribute := item.SelectAttr(consts.XMLNameAttribute)
+ compilerOptions = item.SelectElement(consts.XMLTagNameCompilerOptions)
+ if compilerOptions != nil {
+ msg.Info(" 🔁 Updating %s mode", attribute.Value)
+ processCompilerOptions(compilerOptions)
+ }
+ }
+ }
+ }
- buildModes := projectOptions.SelectElement(consts.XMLTagNameBuildModes)
- for _, item := range buildModes.SelectElements(consts.XMLTagNameItem) {
- attribute := item.SelectAttr(consts.XMLNameAttribute)
- compilerOptions = item.SelectElement(consts.XMLTagNameCompilerOptions)
+ packageOptions := root.SelectElement("Package")
+ if packageOptions != nil {
+ compilerOptions = packageOptions.SelectElement(consts.XMLTagNameCompilerOptions)
if compilerOptions != nil {
- msg.Info(" 🔁 Updating %s mode", attribute.Value)
processCompilerOptions(compilerOptions)
}
}
diff --git a/utils/librarypath/lazarus_test.go b/utils/librarypath/lazarus_test.go
new file mode 100644
index 00000000..3d5f5d91
--- /dev/null
+++ b/utils/librarypath/lazarus_test.go
@@ -0,0 +1,173 @@
+//nolint:testpackage // Testing internal Lazarus 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 mockLpiContent = `
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+const mockLpkContent = `
+
+
+
+
+
+
+
+
+
+`
+
+// TestLazarusPathInjection verifies that other unit search paths are correctly injected in both .lpi and .lpk files.
+func TestLazarusPathInjection(t *testing.T) {
+ tempDir := t.TempDir()
+
+ // Redirect working directory
+ oldWd, err := os.Getwd()
+ if err == nil {
+ defer func() { _ = os.Chdir(oldWd) }()
+ }
+ if err := os.Chdir(tempDir); err != nil {
+ t.Fatalf("Failed to change directory: %v", err)
+ }
+
+ // Initialize package manager
+ fs := filesystem.NewOSFileSystem()
+ packageRepo := repository.NewFilePackageRepository(fs)
+ lockRepo := repository.NewFileLockRepository(fs)
+ packageService := packages.NewPackageService(packageRepo, lockRepo)
+ pkgmanager.SetInstance(packageService)
+
+ // Set up a mock dependency
+ modulesDir := filepath.Join(tempDir, "modules")
+ err = os.MkdirAll(filepath.Join(modulesDir, "horse", "src"), 0755)
+ if err != nil {
+ t.Fatalf("Failed to create modules: %v", err)
+ }
+
+ // Create boss.json for dependency
+ bossJsonContent := `{"name": "horse", "mainsrc": "src"}`
+ err = os.WriteFile(filepath.Join(modulesDir, "horse", "boss.json"), []byte(bossJsonContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write dependency boss.json: %v", err)
+ }
+
+ // Create a dummy source file in the dependency so it matches RegexArtifacts
+ err = os.WriteFile(filepath.Join(modulesDir, "horse", "src", "dummy.pas"), []byte("unit dummy;"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write dummy.pas: %v", err)
+ }
+
+ // 1. Test LPI (Project) Path Injection
+ lpiPath := filepath.Join(tempDir, "project.lpi")
+ err = os.WriteFile(lpiPath, []byte(mockLpiContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write mock LPI: %v", err)
+ }
+
+ updateOtherUnitFilesProject(lpiPath)
+
+ // Verify LPI XML contents
+ doc := etree.NewDocument()
+ if err := doc.ReadFromFile(lpiPath); err != nil {
+ t.Fatalf("Failed to read updated LPI: %v", err)
+ }
+
+ root := doc.Root()
+ compOpts := root.SelectElement("CompilerOptions")
+ if compOpts == nil {
+ t.Fatal("CompilerOptions element not found in LPI")
+ }
+ searchPaths := compOpts.SelectElement("SearchPaths")
+ if searchPaths == nil {
+ t.Fatal("SearchPaths element not found in LPI")
+ }
+ otherUnitFiles := searchPaths.SelectElement("OtherUnitFiles")
+ if otherUnitFiles == nil {
+ t.Fatal("OtherUnitFiles element not found in LPI")
+ }
+ valAttr := otherUnitFiles.SelectAttr("Value")
+ if valAttr == nil {
+ t.Fatal("Value attribute not found in LPI OtherUnitFiles")
+ }
+
+ expectedPath := filepath.Clean("modules/horse/src")
+ if !strings.Contains(filepath.Clean(valAttr.Value), expectedPath) {
+ t.Errorf("Expected path %q to be injected in LPI, got %q", expectedPath, valAttr.Value)
+ }
+
+ // 2. Test LPK (Package) Path Injection
+ lpkPath := filepath.Join(tempDir, "package.lpk")
+ err = os.WriteFile(lpkPath, []byte(mockLpkContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write mock LPK: %v", err)
+ }
+
+ updateOtherUnitFilesProject(lpkPath)
+
+ // Verify LPK XML contents
+ docLpk := etree.NewDocument()
+ if err := docLpk.ReadFromFile(lpkPath); err != nil {
+ t.Fatalf("Failed to read updated LPK: %v", err)
+ }
+
+ rootLpk := docLpk.Root()
+ pkgOpts := rootLpk.SelectElement("Package")
+ if pkgOpts == nil {
+ t.Fatal("Package element not found in LPK")
+ }
+ compOptsLpk := pkgOpts.SelectElement("CompilerOptions")
+ if compOptsLpk == nil {
+ t.Fatal("CompilerOptions element not found in LPK")
+ }
+ searchPathsLpk := compOptsLpk.SelectElement("SearchPaths")
+ if searchPathsLpk == nil {
+ t.Fatal("SearchPaths element not found in LPK")
+ }
+ otherUnitFilesLpk := searchPathsLpk.SelectElement("OtherUnitFiles")
+ if otherUnitFilesLpk == nil {
+ t.Fatal("OtherUnitFiles element not found in LPK")
+ }
+ valAttrLpk := otherUnitFilesLpk.SelectAttr("Value")
+ if valAttrLpk == nil {
+ t.Fatal("Value attribute not found in LPK OtherUnitFiles")
+ }
+
+ if !strings.Contains(filepath.Clean(valAttrLpk.Value), expectedPath) {
+ t.Errorf("Expected path %q to be injected in LPK, got %q", expectedPath, valAttrLpk.Value)
+ }
+}