add per-OS file variants via files.<os>/ directories
This commit is contained in:
+179
-57
@@ -11,42 +11,74 @@ import (
|
||||
)
|
||||
|
||||
func ApplyPackage(filesDir, targetRoot string, cfg *packageConfig) error {
|
||||
return ApplyPackages([]string{filesDir}, targetRoot, cfg)
|
||||
}
|
||||
|
||||
func ApplyPackages(filesDirs []string, targetRoot string, cfg *packageConfig) error {
|
||||
if err := EnsureDir(targetRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path == filesDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(filesDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ShouldIgnorePath(rel, cfg) {
|
||||
if entry.IsDir() {
|
||||
return filepath.SkipDir
|
||||
for _, filesDir := range filesDirs {
|
||||
if _, err := os.Stat(filesDir); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetRoot, rel)
|
||||
err := filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if path == filesDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
return EnsureDir(targetPath)
|
||||
}
|
||||
rel, err := filepath.Rel(filesDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcAbs, err := filepath.Abs(path)
|
||||
if ShouldIgnorePath(rel, cfg) {
|
||||
if entry.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetRoot, rel)
|
||||
|
||||
if entry.IsDir() {
|
||||
return EnsureDir(targetPath)
|
||||
}
|
||||
|
||||
srcAbs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// For overlay mode: remove existing symlink if it points to a different source
|
||||
if info, err := os.Lstat(targetPath); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
current, err := os.Readlink(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != srcAbs {
|
||||
// Remove the old symlink to allow overlay
|
||||
if err := os.Remove(targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LinkFile(srcAbs, targetPath)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return LinkFile(srcAbs, targetPath)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LinkFile(src, dst string) error {
|
||||
@@ -88,68 +120,158 @@ func EnsureDir(path string) error {
|
||||
}
|
||||
|
||||
func FindStaleLinks(filesDir, targetRoot string) ([]string, error) {
|
||||
filesAbs, err := filepath.Abs(filesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FindStaleLinksMulti([]string{filesDir}, targetRoot)
|
||||
}
|
||||
|
||||
func FindStaleLinksMulti(filesDirs []string, targetRoot string) ([]string, error) {
|
||||
targetAbs, err := filepath.Abs(targetRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stale []string
|
||||
walkErr := filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
// Collect all valid source directories (prefixes) for checking
|
||||
var sourcePrefixes []string
|
||||
for _, filesDir := range filesDirs {
|
||||
filesAbs, err := filepath.Abs(filesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(filesAbs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetAbs, rel)
|
||||
info, err := os.Lstat(targetPath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(filesDir); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Symlink doesn't exist - not stale, just not applied
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
sourcePrefixes = append(sourcePrefixes, filesAbs)
|
||||
}
|
||||
|
||||
if len(sourcePrefixes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var stale []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// First pass: walk source directories to find files that exist
|
||||
for _, filesDir := range filesDirs {
|
||||
filesAbs, err := filepath.Abs(filesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
rel, err := filepath.Rel(filesAbs, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetAbs, rel)
|
||||
seen[targetPath] = true
|
||||
|
||||
info, err := os.Lstat(targetPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := os.Readlink(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !filepath.IsAbs(src) {
|
||||
src = filepath.Join(filepath.Dir(targetPath), src)
|
||||
}
|
||||
src = filepath.Clean(src)
|
||||
|
||||
// Check if symlink points to any of our source directories
|
||||
pointsToRepo := false
|
||||
for _, prefix := range sourcePrefixes {
|
||||
if strings.HasPrefix(src, prefix+string(os.PathSeparator)) || src == prefix {
|
||||
pointsToRepo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pointsToRepo {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the source file in repo still exists
|
||||
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
|
||||
stale = append(stale, targetPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: walk target directory to find orphaned symlinks
|
||||
// (symlinks pointing to our repo but source file no longer exists)
|
||||
targetEntries, err := os.ReadDir(targetAbs)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return stale, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range targetEntries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
targetPath := filepath.Join(targetAbs, entry.Name())
|
||||
if seen[targetPath] {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := os.Lstat(targetPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
// Not a symlink, skip
|
||||
return nil
|
||||
continue
|
||||
}
|
||||
|
||||
src, err := os.Readlink(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(src) {
|
||||
src = filepath.Join(filepath.Dir(targetPath), src)
|
||||
}
|
||||
src = filepath.Clean(src)
|
||||
|
||||
// Only consider symlinks pointing to our repo
|
||||
if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs {
|
||||
return nil
|
||||
// Check if symlink points to any of our source directories
|
||||
pointsToRepo := false
|
||||
for _, prefix := range sourcePrefixes {
|
||||
if strings.HasPrefix(src, prefix+string(os.PathSeparator)) || src == prefix {
|
||||
pointsToRepo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pointsToRepo {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the source file in repo still exists
|
||||
// Check if source still exists
|
||||
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
|
||||
stale = append(stale, targetPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if walkErr != nil {
|
||||
return nil, walkErr
|
||||
}
|
||||
|
||||
return stale, nil
|
||||
|
||||
Reference in New Issue
Block a user