refactor: improve copy programs

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/tools/common/copy.go b/tools/common/copy.go
index 3eab443..6b1dfc8 100644
--- a/tools/common/copy.go
+++ b/tools/common/copy.go
@@ -4,13 +4,47 @@
 	"fmt"
 	"io"
 	"io/fs"
-	"log"
 	"os"
 	"sync"
 )
 
+func Copy(src string, dst string, info fs.FileInfo, link bool, verbose bool, wg *sync.WaitGroup) error {
+	defer wg.Done()
+	if !info.Mode().IsRegular() {
+		return fmt.Errorf("failed to copy %q: not a regular file", src)
+	}
+	if link {
+		// hardlink this file
+		if verbose {
+			fmt.Printf("hardlink %v => %v\n", src, dst)
+		}
+		if err := os.Link(src, dst); err != nil {
+			// fallback to copy
+			if verbose {
+				fmt.Printf("hardlink failed: %v\n", err)
+				fmt.Printf("copy (fallback) %v => %v\n", src, dst)
+			}
+			err = copyFile(src, dst)
+			if err != nil {
+				return fmt.Errorf("failed to copy %q: %w", src, err)
+			}
+		}
+	} else {
+		// copy this file
+		if verbose {
+			fmt.Printf("copy %v => %v\n", src, dst)
+		}
+		err := copyFile(src, dst)
+		if err != nil {
+			return fmt.Errorf("failed to copy %q: %w", src, err)
+		}
+	}
+
+	return nil
+}
+
 // From https://opensource.com/article/18/6/copying-files-go
-func CopyFile(src string, dst string) error {
+func copyFile(src string, dst string) error {
 	source, err := os.Open(src)
 	if err != nil {
 		return err
@@ -22,42 +56,10 @@
 		return err
 	}
 	defer destination.Close()
-	_, err = io.Copy(destination, source)
-	return err
-}
 
-func Copy(src string, dst string, info fs.FileInfo, link bool, verbose bool, wg *sync.WaitGroup) {
-	if wg != nil {
-		defer wg.Done()
+	if _, err := io.Copy(destination, source); err != nil {
+		return err
 	}
-	if !info.Mode().IsRegular() {
-		log.Fatalf("%s is not a regular file", src)
-	}
-	if link {
-		// hardlink this file
-		if verbose {
-			fmt.Printf("hardlink %v => %v\n", src, dst)
-		}
-		err := os.Link(src, dst)
-		if err != nil {
-			// fallback to copy
-			if verbose {
-				fmt.Printf("hardlink failed: %v\n", err)
-				fmt.Printf("copy (fallback) %v => %v\n", src, dst)
-			}
-			err = CopyFile(src, dst)
-			if err != nil {
-				log.Fatal(err)
-			}
-		}
-	} else {
-		// copy this file
-		if verbose {
-			fmt.Printf("copy %v => %v\n", src, dst)
-		}
-		err := CopyFile(src, dst)
-		if err != nil {
-			log.Fatal(err)
-		}
-	}
+
+	return nil
 }
diff --git a/tools/copy_directory/main.go b/tools/copy_directory/main.go
index 6bac13e..2e8dafb 100644
--- a/tools/copy_directory/main.go
+++ b/tools/copy_directory/main.go
@@ -1,25 +1,23 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"io/fs"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
 	"sync"
+	"sync/atomic"
 
 	"github.com/aspect-build/bazel-lib/tools/common"
 )
 
-type pathSet map[string]bool
-
-var srcPaths = pathSet{}
+var srcPaths = make(map[string]struct{})
 var copyWaitGroup sync.WaitGroup
-var hardlink = false
-var verbose = false
+var hasErrors atomic.Bool
 
-func copyDir(src string, dst string) error {
+func copyDir(src string, dst string, hardlink bool, verbose bool, errors chan<- error) error {
 	// filepath.WalkDir walks the file tree rooted at root, calling fn for each file or directory in
 	// the tree, including root. See https://pkg.go.dev/path/filepath#WalkDir for more info.
 	return filepath.WalkDir(src, func(p string, dirEntry fs.DirEntry, err error) error {
@@ -27,16 +25,23 @@
 			return err
 		}
 
+		// Gracefully stop the walking if an error has been reported.
+		if hasErrors.Load() {
+			return nil
+		}
+
+		copySrc := p
+
 		r, err := filepath.Rel(src, p)
 		if err != nil {
 			return err
 		}
 
-		d := filepath.Join(dst, r)
+		copyDst := filepath.Join(dst, r)
 
 		if dirEntry.IsDir() {
-			srcPaths[src] = true
-			return os.MkdirAll(d, os.ModePerm)
+			srcPaths[src] = struct{}{}
+			return os.MkdirAll(copyDst, os.ModePerm)
 		}
 
 		info, err := dirEntry.Info()
@@ -53,7 +58,7 @@
 			if !path.IsAbs(linkPath) {
 				linkPath = path.Join(path.Dir(p), linkPath)
 			}
-			if srcPaths[linkPath] {
+			if _, isRecursive := srcPaths[linkPath]; isRecursive {
 				// recursive symlink; silently ignore
 				return nil
 			}
@@ -63,52 +68,62 @@
 			}
 			if stat.IsDir() {
 				// symlink points to a directory
-				return copyDir(linkPath, d)
+				return copyDir(linkPath, copyDst, hardlink, verbose, errors)
 			} else {
 				// symlink points to a regular file
-				copyWaitGroup.Add(1)
-				go common.Copy(linkPath, d, stat, hardlink, verbose, &copyWaitGroup)
-				return nil
+				copySrc = linkPath
 			}
 		}
 
 		// a regular file
 		copyWaitGroup.Add(1)
-		go common.Copy(p, d, info, hardlink, verbose, &copyWaitGroup)
+		go func() {
+			if err := common.Copy(copySrc, copyDst, info, hardlink, verbose, &copyWaitGroup); err != nil {
+				errors <- err
+			}
+		}()
 		return nil
 	})
 }
 
 func main() {
 	args := os.Args[1:]
-
-	if len(args) == 1 {
-		if args[0] == "--version" || args[0] == "-v" {
-			fmt.Printf("copy_directory %s\n", common.Version())
-			return
-		}
+	if len(args) == 1 && (args[0] == "--version" || args[0] == "-v") {
+		fmt.Printf("copy_directory %s\n", common.Version())
+		return
 	}
 
-	if len(args) < 2 {
+	var hardlink bool
+	var verbose bool
+
+	flag.BoolVar(&hardlink, "hardlink", false, "use hardlinks instead of copying files")
+	flag.BoolVar(&verbose, "verbose", false, "print verbose output")
+	flag.Parse()
+
+	if flag.NArg() < 2 {
 		fmt.Println("Usage: copy_directory src dst [--hardlink] [--verbose]")
 		os.Exit(1)
 	}
 
-	src := args[0]
-	dst := args[1]
+	src := flag.Arg(0)
+	dst := flag.Arg(1)
 
-	if len(args) > 2 {
-		for _, a := range os.Args[2:] {
-			if a == "--hardlink" {
-				hardlink = true
-			} else if a == "--verbose" {
-				verbose = true
-			}
+	errors := make(chan error, 100)
+
+	go func() {
+		if err := copyDir(src, dst, hardlink, verbose, errors); err != nil {
+			errors <- err
 		}
+		copyWaitGroup.Wait()
+		close(errors)
+	}()
+
+	for err := range errors {
+		hasErrors.Store(true)
+		fmt.Fprintln(os.Stderr, err)
 	}
 
-	if err := copyDir(src, dst); err != nil {
-		log.Fatal(err)
+	if hasErrors.Load() {
+		os.Exit(1)
 	}
-	copyWaitGroup.Wait()
 }
diff --git a/tools/copy_to_directory/main.go b/tools/copy_to_directory/main.go
index 7d1e15d..731e2eb 100644
--- a/tools/copy_to_directory/main.go
+++ b/tools/copy_to_directory/main.go
@@ -3,8 +3,8 @@
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/fs"
-	"io/ioutil"
 	"log"
 	"os"
 	"path"
@@ -59,7 +59,7 @@
 	}
 	defer f.Close()
 
-	byteValue, err := ioutil.ReadAll(f)
+	byteValue, err := io.ReadAll(f)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read config file: %w", err)
 	}
@@ -262,10 +262,7 @@
 		return err
 	}
 	if rootPathMatch != "" {
-		outputPath = outputPath[len(rootPathMatch):]
-		if strings.HasPrefix(outputPath, "/") {
-			outputPath = outputPath[1:]
-		}
+		outputPath = strings.TrimPrefix(filepath.Clean(outputPath[len(rootPathMatch):]), string(filepath.Separator))
 	}
 
 	// apply include_srcs_patterns