github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/build/filegroup.go (about)

     1  // Logic relating to building filegroups.
     2  //
     3  // Unlike most targets, filegroups are special in that (1) they are known to the
     4  // system and have a custom implementation and (2) multiple filegroups can output
     5  // the same file. This does lead to a potential race condition where we have to
     6  // be sure to build each output file only once.
     7  // Currently this is implemented by a single thread that builds them all; there
     8  // are other schemes we could have but this is simple enough (and since we link
     9  // them rather than copying there should not be a lot of I/O wait).
    10  
    11  package build
    12  
    13  import (
    14  	"encoding/base64"
    15  	"os"
    16  	"path"
    17  	"sync"
    18  
    19  	"core"
    20  	"fs"
    21  )
    22  
    23  func init() {
    24  	theFilegroupBuilder = &filegroupBuilder{
    25  		built: map[string]bool{},
    26  	}
    27  }
    28  
    29  // A filegroupBuilder is a singleton that we have that builds all filegroups.
    30  // This works around the problem where multiple filegroups can output the same
    31  // file, which means that if built simultaneously they can fight with one another.
    32  type filegroupBuilder struct {
    33  	mutex sync.Mutex
    34  	built map[string]bool
    35  }
    36  
    37  var theFilegroupBuilder *filegroupBuilder
    38  
    39  // Build builds a single filegroup file.
    40  func (builder *filegroupBuilder) Build(target *core.BuildTarget, from, to string) error {
    41  	builder.mutex.Lock()
    42  	defer builder.mutex.Unlock()
    43  	if builder.built[to] {
    44  		return nil // File's already been built.
    45  	}
    46  	if fs.IsSameFile(from, to) {
    47  		// File exists already and is the same file. Nothing to do.
    48  		// TODO(peterebden): This should also have a recursive case for when it's a directory...
    49  		builder.built[to] = true
    50  		return nil
    51  	}
    52  	// Must actually build the file.
    53  	if err := os.RemoveAll(to); err != nil {
    54  		return err
    55  	} else if err := fs.EnsureDir(to); err != nil {
    56  		return err
    57  	} else if err := core.RecursiveCopyFile(from, to, target.OutMode(), true, false); err != nil {
    58  		return err
    59  	}
    60  	builder.built[to] = true
    61  	movePathHash(from, to, true) // In case we've already hashed the source, don't do it again.
    62  	return nil
    63  }
    64  
    65  // buildFilegroup runs the manual build steps for a filegroup rule.
    66  // We don't force this to be done in bash to avoid errors with maximum command lengths,
    67  // and it's actually quite fiddly to get just so there.
    68  func buildFilegroup(tid int, state *core.BuildState, target *core.BuildTarget) error {
    69  	if err := prepareDirectory(target.OutDir(), false); err != nil {
    70  		return err
    71  	}
    72  	if err := os.RemoveAll(ruleHashFileName(target)); err != nil {
    73  		return err
    74  	}
    75  	outDir := target.OutDir()
    76  	localSources := target.AllLocalSourcePaths(state.Graph)
    77  	for i, source := range target.AllFullSourcePaths(state.Graph) {
    78  		out, _ := filegroupOutputPath(target, outDir, localSources[i], source)
    79  		if err := theFilegroupBuilder.Build(target, source, out); err != nil {
    80  			return err
    81  		}
    82  	}
    83  	if target.HasLabel("py") && !target.IsBinary {
    84  		// Pre-emptively create __init__.py files so the outputs can be loaded dynamically.
    85  		// It's a bit cheeky to do non-essential language-specific logic but this enables
    86  		// a lot of relatively normal Python workflows.
    87  		// Errors are deliberately ignored.
    88  		if pkg := state.Graph.Package(target.Label.PackageName); pkg == nil || !pkg.HasOutput("__init__.py") {
    89  			// Don't create this if someone else is going to create this in the package.
    90  			createInitPy(outDir)
    91  		}
    92  	}
    93  	return nil
    94  }
    95  
    96  // copyFilegroupHashes copies the hashes of the inputs of this filegroup to their outputs.
    97  // This is a small optimisation to ensure we don't need to recalculate them unnecessarily.
    98  func copyFilegroupHashes(state *core.BuildState, target *core.BuildTarget) {
    99  	outDir := target.OutDir()
   100  	localSources := target.AllLocalSourcePaths(state.Graph)
   101  	for i, source := range target.AllFullSourcePaths(state.Graph) {
   102  		if out, _ := filegroupOutputPath(target, outDir, localSources[i], source); out != source {
   103  			movePathHash(source, out, true)
   104  		}
   105  	}
   106  }
   107  
   108  // updateHashFilegroupPaths sets the output paths on a hash_filegroup rule.
   109  // Unlike normal filegroups, hash filegroups can't calculate these themselves very readily.
   110  func updateHashFilegroupPaths(state *core.BuildState, target *core.BuildTarget) {
   111  	outDir := target.OutDir()
   112  	localSources := target.AllLocalSourcePaths(state.Graph)
   113  	for i, source := range target.AllFullSourcePaths(state.Graph) {
   114  		_, relOut := filegroupOutputPath(target, outDir, localSources[i], source)
   115  		target.AddOutput(relOut)
   116  	}
   117  }
   118  
   119  // filegroupOutputPath returns the output path for a single filegroup source.
   120  func filegroupOutputPath(target *core.BuildTarget, outDir, source, full string) (string, string) {
   121  	if !target.IsHashFilegroup {
   122  		return path.Join(outDir, source), source
   123  	}
   124  	// Hash filegroups have a hash embedded into the output name.
   125  	ext := path.Ext(source)
   126  	before := source[:len(source)-len(ext)]
   127  	hash, err := pathHash(full, false)
   128  	if err != nil {
   129  		panic(err)
   130  	}
   131  	out := before + "-" + base64.RawURLEncoding.EncodeToString(hash) + ext
   132  	return path.Join(outDir, out), out
   133  }
   134  
   135  func createInitPy(dir string) {
   136  	initPy := path.Join(dir, "__init__.py")
   137  	if core.PathExists(initPy) {
   138  		return
   139  	}
   140  	if f, err := os.OpenFile(initPy, os.O_RDONLY|os.O_CREATE, 0444); err == nil {
   141  		f.Close()
   142  	}
   143  	dir = path.Dir(dir)
   144  	if dir != core.GenDir && dir != "." && !core.PathExists(path.Join(dir, "__init__.py")) {
   145  		createInitPy(dir)
   146  	}
   147  }