2023-01-02 00:14:48 +00:00
package main
2023-01-03 17:42:20 +00:00
import (
"encoding/json"
"fmt"
2023-01-06 18:29:45 +00:00
"io/fs"
2023-01-03 17:42:20 +00:00
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
2023-01-15 20:17:30 +00:00
"github.com/aspect-build/bazel-lib/tools/common"
2023-01-05 22:24:24 +00:00
"github.com/bmatcuk/doublestar/v4"
2023-01-03 17:42:20 +00:00
"golang.org/x/exp/maps"
)
type fileInfo struct {
2023-01-06 18:29:45 +00:00
Package string ` json:"package" `
Path string ` json:"path" `
RootPath string ` json:"root_path" `
ShortPath string ` json:"short_path" `
Workspace string ` json:"workspace" `
WorkspacePath string ` json:"workspace_path" `
2023-01-14 15:55:23 +00:00
Hardlink bool ` json:"hardlink" `
2023-01-06 18:29:45 +00:00
FileInfo fs . FileInfo
2023-01-03 17:42:20 +00:00
}
type config struct {
AllowOverwrites bool ` json:"allow_overwrites" `
Dst string ` json:"dst" `
ExcludeSrcsPackages [ ] string ` json:"exclude_srcs_packages" `
ExcludeSrcsPatterns [ ] string ` json:"exclude_srcs_patterns" `
Files [ ] fileInfo ` json:"files" `
IncludeExternalRepositories [ ] string ` json:"include_external_repositories" `
IncludeSrcsPackages [ ] string ` json:"include_srcs_packages" `
IncludeSrcsPatterns [ ] string ` json:"include_srcs_patterns" `
ReplacePrefixes map [ string ] string ` json:"replace_prefixes" `
RootPaths [ ] string ` json:"root_paths" `
Verbose bool ` json:"verbose" `
2023-01-05 22:24:24 +00:00
ReplacePrefixesKeys [ ] string
2023-01-03 17:42:20 +00:00
}
2023-01-06 18:29:45 +00:00
type copyMap map [ string ] fileInfo
type pathSet map [ string ] bool
2023-01-03 17:42:20 +00:00
func parseConfig ( configPath string ) ( * config , error ) {
f , err := os . Open ( configPath )
if err != nil {
return nil , fmt . Errorf ( "failed to open config file: %w" , err )
}
defer f . Close ( )
byteValue , err := ioutil . ReadAll ( f )
if err != nil {
return nil , fmt . Errorf ( "failed to read config file: %w" , err )
}
var cfg config
if err := json . Unmarshal ( [ ] byte ( byteValue ) , & cfg ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse config file: %w" , err )
}
cfg . ReplacePrefixesKeys = maps . Keys ( cfg . ReplacePrefixes )
return & cfg , nil
}
2023-01-05 22:24:24 +00:00
func anyGlobsMatch ( globs [ ] string , test string ) ( bool , error ) {
for _ , g := range globs {
match , err := doublestar . Match ( g , test )
2023-01-03 17:42:20 +00:00
if err != nil {
2023-01-05 22:24:24 +00:00
return false , err
2023-01-03 17:42:20 +00:00
}
2023-01-05 22:24:24 +00:00
if match {
return true , nil
2023-01-03 17:42:20 +00:00
}
}
2023-01-05 22:24:24 +00:00
return false , nil
2023-01-03 17:42:20 +00:00
}
2023-01-05 22:24:24 +00:00
func longestGlobsMatch ( globs [ ] string , test string ) ( string , int , error ) {
2023-01-03 17:42:20 +00:00
result := ""
index := 0
for i , g := range globs {
2023-01-05 22:24:24 +00:00
match , err := longestGlobMatch ( g , test )
if err != nil {
return "" , 0 , err
}
2023-01-03 17:42:20 +00:00
if len ( match ) > len ( result ) {
result = match
index = i
}
}
2023-01-05 22:24:24 +00:00
return result , index , nil
2023-01-03 17:42:20 +00:00
}
2023-01-05 22:24:24 +00:00
func longestGlobMatch ( g string , test string ) ( string , error ) {
2023-01-03 17:42:20 +00:00
for i := 0 ; i < len ( test ) ; i ++ {
t := test [ : len ( test ) - i ]
2023-01-05 22:24:24 +00:00
match , err := doublestar . Match ( g , t )
if err != nil {
return "" , err
}
if match {
return t , nil
2023-01-03 17:42:20 +00:00
}
}
2023-01-05 22:24:24 +00:00
return "" , nil
2023-01-03 17:42:20 +00:00
}
2023-01-06 18:29:45 +00:00
func calcCopyDir ( cfg * config , copyPaths copyMap , srcPaths pathSet , file fileInfo ) error {
if srcPaths == nil {
srcPaths = pathSet { }
}
srcPaths [ file . Path ] = true
// 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.
2023-01-15 20:17:30 +00:00
return filepath . WalkDir ( file . Path , func ( p string , dirEntry fs . DirEntry , err error ) error {
if err != nil {
return err
}
if dirEntry . IsDir ( ) {
2023-01-06 18:29:45 +00:00
// remember that this directory was visited to prevent infinite recursive symlink loops and
// then short-circuit by returning nil since filepath.Walk will visit files contained within
// this directory automatically
srcPaths [ p ] = true
return nil
2023-01-03 17:42:20 +00:00
}
2023-01-06 18:29:45 +00:00
2023-01-15 20:17:30 +00:00
info , err := dirEntry . Info ( )
if err != nil {
return err
}
2023-01-06 18:29:45 +00:00
if info . Mode ( ) & os . ModeSymlink == os . ModeSymlink {
2023-01-14 23:51:01 +00:00
// symlink to directories are intentionally never followed by filepath.Walk to avoid infinite recursion
2023-01-06 18:29:45 +00:00
linkPath , err := os . Readlink ( p )
if err != nil {
return err
}
if ! path . IsAbs ( linkPath ) {
linkPath = path . Join ( path . Dir ( p ) , linkPath )
}
if srcPaths [ linkPath ] {
// recursive symlink; silently ignore
return nil
}
stat , err := os . Stat ( linkPath )
if err != nil {
2023-01-14 23:51:01 +00:00
return fmt . Errorf ( "failed to stat file %s pointed to by symlink %s: %w" , linkPath , p , err )
2023-01-06 18:29:45 +00:00
}
if stat . IsDir ( ) {
2023-01-14 23:51:01 +00:00
// symlink points to a directory
2023-01-06 18:29:45 +00:00
f := fileInfo {
Package : file . Package ,
Path : linkPath ,
RootPath : file . RootPath ,
ShortPath : path . Join ( file . ShortPath ) ,
Workspace : file . Workspace ,
WorkspacePath : path . Join ( file . WorkspacePath ) ,
2023-01-14 15:55:23 +00:00
Hardlink : file . Hardlink ,
2023-01-06 18:29:45 +00:00
FileInfo : stat ,
}
return calcCopyDir ( cfg , copyPaths , srcPaths , f )
} else {
2023-01-14 23:51:01 +00:00
// symlink points to a regular file
2023-01-06 18:29:45 +00:00
r , err := filepath . Rel ( file . Path , p )
if err != nil {
return fmt . Errorf ( "failed to walk directory %s: %w" , file . Path , err )
}
f := fileInfo {
Package : file . Package ,
Path : linkPath ,
RootPath : file . RootPath ,
ShortPath : path . Join ( file . ShortPath , r ) ,
Workspace : file . Workspace ,
WorkspacePath : path . Join ( file . WorkspacePath , r ) ,
2023-01-14 15:55:23 +00:00
Hardlink : file . Hardlink ,
2023-01-06 18:29:45 +00:00
FileInfo : stat ,
}
return calcCopyPath ( cfg , copyPaths , f )
}
}
// a regular file
r , err := filepath . Rel ( file . Path , p )
if err != nil {
return fmt . Errorf ( "failed to walk directory %s: %w" , file . Path , err )
}
f := fileInfo {
Package : file . Package ,
Path : p ,
RootPath : file . RootPath ,
ShortPath : path . Join ( file . ShortPath , r ) ,
Workspace : file . Workspace ,
WorkspacePath : path . Join ( file . WorkspacePath , r ) ,
2023-01-14 15:55:23 +00:00
Hardlink : file . Hardlink ,
2023-01-06 18:29:45 +00:00
FileInfo : info ,
}
return calcCopyPath ( cfg , copyPaths , f )
2023-01-03 17:42:20 +00:00
} )
}
2023-01-06 18:29:45 +00:00
func calcCopyPath ( cfg * config , copyPaths copyMap , file fileInfo ) error {
2023-01-03 17:42:20 +00:00
// Apply filters and transformations in the following order:
//
// - `include_external_repositories`
// - `include_srcs_packages`
// - `exclude_srcs_packages`
// - `root_paths`
// - `include_srcs_patterns`
// - `exclude_srcs_patterns`
// - `replace_prefixes`
//
// If you change this order please update the docstrings in the copy_to_directory rule.
outputPath := file . WorkspacePath
outputRoot := path . Dir ( outputPath )
// apply include_external_repositories (if the file is from an external repository)
if file . Workspace != "" {
2023-01-05 22:24:24 +00:00
match , err := anyGlobsMatch ( cfg . IncludeExternalRepositories , file . Workspace )
if err != nil {
return err
}
if ! match {
2023-01-03 17:42:20 +00:00
return nil // external workspace is not included
}
}
// apply include_srcs_packages
2023-01-05 22:24:24 +00:00
match , err := anyGlobsMatch ( cfg . IncludeSrcsPackages , file . Package )
if err != nil {
return err
}
if ! match {
2023-01-03 17:42:20 +00:00
return nil // package is not included
}
// apply exclude_srcs_packages
2023-01-05 22:24:24 +00:00
match , err = anyGlobsMatch ( cfg . ExcludeSrcsPackages , file . Package )
if err != nil {
return err
}
if match {
2023-01-03 17:42:20 +00:00
return nil // package is excluded
}
// apply root_paths
2023-01-05 22:24:24 +00:00
rootPathMatch , _ , err := longestGlobsMatch ( cfg . RootPaths , outputRoot )
if err != nil {
return err
}
2023-01-03 17:42:20 +00:00
if rootPathMatch != "" {
outputPath = outputPath [ len ( rootPathMatch ) : ]
if strings . HasPrefix ( outputPath , "/" ) {
outputPath = outputPath [ 1 : ]
}
}
// apply include_srcs_patterns
2023-01-05 22:24:24 +00:00
match , err = anyGlobsMatch ( cfg . IncludeSrcsPatterns , outputPath )
if err != nil {
return err
}
if ! match {
2023-01-03 17:42:20 +00:00
return nil // outputPath is not included
}
2023-01-05 22:24:24 +00:00
// apply exclude_srcs_patterns
match , err = anyGlobsMatch ( cfg . ExcludeSrcsPatterns , outputPath )
if err != nil {
return err
}
if match {
2023-01-03 17:42:20 +00:00
return nil // outputPath is excluded
}
// apply replace_prefixes
2023-01-05 22:24:24 +00:00
replacePrefixMatch , replacePrefixIndex , err := longestGlobsMatch ( cfg . ReplacePrefixesKeys , outputPath )
if err != nil {
return err
}
2023-01-03 17:42:20 +00:00
if replacePrefixMatch != "" {
replaceWith := cfg . ReplacePrefixes [ cfg . ReplacePrefixesKeys [ replacePrefixIndex ] ]
outputPath = replaceWith + outputPath [ len ( replacePrefixMatch ) : ]
}
outputPath = path . Join ( cfg . Dst , outputPath )
// add this file to the copy Paths
dup , exists := copyPaths [ outputPath ]
if exists {
if dup . ShortPath == file . ShortPath {
2023-01-06 18:29:45 +00:00
if file . FileInfo . Size ( ) == dup . FileInfo . Size ( ) && file . RootPath == "" {
// this is likely the same file listed twice: the original in the source tree and the copy
// in the output tree; when this happens prefer the output tree copy.
2023-01-03 17:42:20 +00:00
return nil
}
} else if ! cfg . AllowOverwrites {
return fmt . Errorf ( "duplicate output file '%s' configured from source files '%s' and '%s'; set 'allow_overwrites' to True to allow this overwrites but keep in mind that order matters when this is set" , outputPath , dup . Path , file . Path )
}
}
copyPaths [ outputPath ] = file
return nil
}
2023-01-06 18:29:45 +00:00
func calcCopyPaths ( cfg * config ) ( copyMap , error ) {
copyPaths := copyMap { }
2023-01-03 17:42:20 +00:00
for _ , file := range cfg . Files {
2023-01-06 18:29:45 +00:00
stat , err := os . Stat ( file . Path )
if err != nil {
return nil , fmt . Errorf ( "failed to stat file %s: %w" , file . Path , err )
}
file . FileInfo = stat
if file . FileInfo . IsDir ( ) {
if err := calcCopyDir ( cfg , copyPaths , nil , file ) ; err != nil {
return nil , err
2023-01-03 17:42:20 +00:00
}
2023-01-06 18:29:45 +00:00
} else {
if err := calcCopyPath ( cfg , copyPaths , file ) ; err != nil {
return nil , err
2023-01-03 17:42:20 +00:00
}
}
}
2023-01-06 18:29:45 +00:00
return copyPaths , nil
2023-01-03 17:42:20 +00:00
}
2023-01-15 20:17:30 +00:00
func main ( ) {
args := os . Args [ 1 : ]
2023-01-03 20:26:51 +00:00
2023-01-15 20:17:30 +00:00
if len ( args ) == 1 {
if args [ 0 ] == "--version" || args [ 0 ] == "-v" {
fmt . Printf ( "copy_to_directory %s\n" , common . Version ( ) )
return
2023-01-03 20:26:51 +00:00
}
}
2023-01-15 20:17:30 +00:00
if len ( args ) != 1 {
fmt . Println ( "Usage: copy_to_directory config_file" )
2023-01-03 20:26:51 +00:00
os . Exit ( 1 )
}
2023-01-15 20:17:30 +00:00
cfg , err := parseConfig ( args [ 0 ] )
2023-01-03 17:42:20 +00:00
if err != nil {
log . Fatal ( err )
}
// Calculate copy paths
copyPaths , err := calcCopyPaths ( cfg )
if err != nil {
log . Fatal ( err )
}
// Perform copies
// TODO: split out into parallel go routines?
for to , from := range copyPaths {
err := os . MkdirAll ( path . Dir ( to ) , os . ModePerm )
if err != nil {
log . Fatal ( err )
}
2023-01-15 20:17:30 +00:00
common . Copy ( from . Path , to , from . FileInfo , from . Hardlink , cfg . Verbose , nil )
2023-01-03 17:42:20 +00:00
}
2023-01-02 00:14:48 +00:00
}