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 }