github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/core/package.go (about) 1 package core 2 3 import ( 4 "fmt" 5 "path" 6 "sort" 7 "strings" 8 "sync" 9 10 "fs" 11 ) 12 13 // Package is a representation of a package, ie. the part of the system (one or more 14 // directories) covered by a single build file. 15 type Package struct { 16 // Name of the package, ie. //spam/eggs 17 Name string 18 // Filename of the build file that defined this package 19 Filename string 20 // Subincluded build defs files that this package imported 21 Subincludes []BuildLabel 22 // If the package is in a subrepo, this is the subrepo it belongs to. It's nil if not. 23 Subrepo *Subrepo 24 // Targets contained within the package 25 targets map[string]*BuildTarget 26 // Set of output files from rules. 27 Outputs map[string]*BuildTarget 28 // Protects access to above 29 mutex sync.RWMutex 30 // Targets whose dependencies got modified during a pre or post-build function. 31 modifiedTargets map[*BuildTarget]struct{} 32 // Used to arbitrate a single post-build function running at a time. 33 // It would be sort of conceptually nice if they ran simultaneously but it'd 34 // be far too hard to ensure consistency in any case where they can interact with one another. 35 buildCallbackMutex sync.Mutex 36 } 37 38 // NewPackage constructs a new package with the given name. 39 func NewPackage(name string) *Package { 40 return &Package{ 41 Name: name, 42 targets: map[string]*BuildTarget{}, 43 Outputs: map[string]*BuildTarget{}, 44 } 45 } 46 47 // Target returns the target with the given name, or nil if this package doesn't have one. 48 func (pkg *Package) Target(name string) *BuildTarget { 49 pkg.mutex.RLock() 50 defer pkg.mutex.RUnlock() 51 return pkg.targets[name] 52 } 53 54 // TargetOrDie returns the target with the given name, and dies if this package doesn't have one. 55 func (pkg *Package) TargetOrDie(name string) *BuildTarget { 56 t := pkg.Target(name) 57 if t == nil { 58 log.Fatalf("Target %s not registered in package %s", name, pkg.Name) 59 } 60 return t 61 } 62 63 // SourceRoot returns the root directory of source files for this package. 64 // This is equivalent to .Name for in-repo packages but differs for those in subrepos. 65 func (pkg *Package) SourceRoot() string { 66 if pkg.Subrepo != nil { 67 return pkg.Subrepo.Dir(pkg.Name) 68 } 69 return pkg.Name 70 } 71 72 // AddTarget adds a new target to this package with the given name. 73 // It doesn't check for duplicates. 74 func (pkg *Package) AddTarget(target *BuildTarget) { 75 pkg.mutex.Lock() 76 defer pkg.mutex.Unlock() 77 pkg.targets[target.Label.Name] = target 78 } 79 80 // AllTargets returns the current set of targets in this package. 81 // This is threadsafe, unlike iterating .Targets directly which is not. 82 func (pkg *Package) AllTargets() []*BuildTarget { 83 pkg.mutex.Lock() 84 defer pkg.mutex.Unlock() 85 ret := make([]*BuildTarget, 0, len(pkg.targets)) 86 for _, target := range pkg.targets { 87 ret = append(ret, target) 88 } 89 return ret 90 } 91 92 // NumTargets returns the number of targets currently registered in this package. 93 func (pkg *Package) NumTargets() int { 94 pkg.mutex.Lock() 95 defer pkg.mutex.Unlock() 96 return len(pkg.targets) 97 } 98 99 // RegisterSubinclude adds a new subinclude to this package, guaranteeing uniqueness. 100 func (pkg *Package) RegisterSubinclude(label BuildLabel) { 101 if !pkg.HasSubinclude(label) { 102 pkg.Subincludes = append(pkg.Subincludes, label) 103 } 104 } 105 106 // HasSubinclude returns true if the package has subincluded the given label. 107 func (pkg *Package) HasSubinclude(label BuildLabel) bool { 108 for _, l := range pkg.Subincludes { 109 if l == label { 110 return true 111 } 112 } 113 return false 114 } 115 116 // HasOutput returns true if the package has the given file as an output. 117 func (pkg *Package) HasOutput(output string) bool { 118 pkg.mutex.RLock() 119 defer pkg.mutex.RUnlock() 120 _, present := pkg.Outputs[output] 121 return present 122 } 123 124 // RegisterOutput registers a new output file in the map. 125 // Returns an error if the file has already been registered. 126 func (pkg *Package) RegisterOutput(fileName string, target *BuildTarget) error { 127 pkg.mutex.Lock() 128 defer pkg.mutex.Unlock() 129 originalFileName := fileName 130 if target.IsBinary { 131 fileName = ":_bin_" + fileName // Add some arbitrary prefix so they don't clash. 132 } 133 if existing, present := pkg.Outputs[fileName]; present && existing != target { 134 if existing.IsFilegroup && !target.IsFilegroup { 135 // Update the existing one with this, the registered outputs should prefer non-filegroup targets. 136 pkg.Outputs[fileName] = target 137 } else if !target.IsFilegroup && !existing.IsFilegroup { 138 return fmt.Errorf("rules %s and %s in %s both attempt to output the same file: %s", 139 existing.Label, target.Label, pkg.Filename, originalFileName) 140 } 141 } 142 pkg.Outputs[fileName] = target 143 return nil 144 } 145 146 // MustRegisterOutput registers a new output file and panics if it's already been registered. 147 func (pkg *Package) MustRegisterOutput(fileName string, target *BuildTarget) { 148 if err := pkg.RegisterOutput(fileName, target); err != nil { 149 panic(err) 150 } 151 } 152 153 // AllChildren returns all child targets of the given one. 154 // The given target is included as well. 155 func (pkg *Package) AllChildren(target *BuildTarget) []*BuildTarget { 156 ret := BuildTargets{} 157 parent := target.Label.Parent() 158 for _, t := range pkg.targets { 159 if t.Label.Parent() == parent { 160 ret = append(ret, t) 161 } 162 } 163 sort.Sort(ret) 164 return ret 165 } 166 167 // IsIncludedIn returns true if the given build label would include this package. 168 // e.g. //src/... includes the packages src and src/core but not src2. 169 func (pkg *Package) IsIncludedIn(label BuildLabel) bool { 170 return pkg.Name == label.PackageName || strings.HasPrefix(pkg.Name, label.PackageName+"/") 171 } 172 173 // EnterBuildCallback is used to arbitrate access to build callbacks & track changes to targets. 174 // The supplied function will be called & a set of modified targets, along with any errors, is returned. 175 func (pkg *Package) EnterBuildCallback(f func() error) (map[*BuildTarget]struct{}, error) { 176 pkg.buildCallbackMutex.Lock() 177 defer pkg.buildCallbackMutex.Unlock() 178 m := map[*BuildTarget]struct{}{} 179 pkg.modifiedTargets = m 180 err := f() 181 pkg.modifiedTargets = nil 182 return m, err 183 } 184 185 // MarkTargetModified marks a single target as being modified during a pre- or post- build function. 186 // Correct usage of EnterBuildCallback must have been observed. 187 func (pkg *Package) MarkTargetModified(target *BuildTarget) { 188 if pkg.modifiedTargets != nil { 189 pkg.modifiedTargets[target] = struct{}{} 190 } 191 } 192 193 // VerifyOutputs checks all files output from this package and verifies that they're all OK; 194 // notably it checks that if targets that output into a subdirectory, that subdirectory isn't 195 // created by another target. That kind of thing can lead to subtle and annoying bugs. 196 // It logs detected warnings to stdout. 197 func (pkg *Package) VerifyOutputs() { 198 for _, warning := range pkg.verifyOutputs() { 199 log.Warning("%s: %s", pkg.Filename, warning) 200 } 201 } 202 203 func (pkg *Package) verifyOutputs() []string { 204 pkg.mutex.RLock() 205 defer pkg.mutex.RUnlock() 206 ret := []string{} 207 for filename, target := range pkg.Outputs { 208 for dir := path.Dir(filename); dir != "."; dir = path.Dir(dir) { 209 if target2, present := pkg.Outputs[dir]; present && target2 != target && !target.HasDependency(target2.Label.Parent()) { 210 ret = append(ret, fmt.Sprintf("Target %s outputs files into the directory %s, which is separately output by %s. This can cause errors based on build order - you should add a dependency.", target.Label, dir, target2.Label)) 211 } 212 } 213 } 214 return ret 215 } 216 217 // FindOwningPackages returns build labels corresponding to the packages that own each of the given files. 218 func FindOwningPackages(state *BuildState, files []string) []BuildLabel { 219 ret := make([]BuildLabel, len(files)) 220 for i, file := range files { 221 ret[i] = FindOwningPackage(state, file) 222 } 223 return ret 224 } 225 226 // FindOwningPackage returns a build label identifying the package that owns a given file. 227 func FindOwningPackage(state *BuildState, file string) BuildLabel { 228 f := file 229 for f != "." { 230 f = path.Dir(f) 231 if fs.IsPackage(state.Config.Parse.BuildFileName, f) { 232 return BuildLabel{PackageName: f, Name: "all"} 233 } 234 } 235 log.Fatalf("No BUILD file owns file %s", file) 236 return BuildLabel{} 237 }