github.com/charypar/monobuild@v0.0.0-20211122220434-fd884ed50212/manifests/manifests.go (about) 1 package manifests 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/charypar/monobuild/graph" 11 ) 12 13 // Kind of the dependency (enum) 14 type Kind int 15 16 // Weak dependency is one, changes to which cause rebuild of its dependents, 17 // but dependents builds can run in parallel with its build 18 var Weak Kind = 1 19 20 // Strong dependency is one, changes to which cause rebuild of its dependentes, 21 // and the dependent builds can only be run when its build successfully finishes 22 var Strong Kind = 2 23 24 // Dependency holds information about the dependency relationship of one 25 // component with another. Dependencies hav a string name and a kind, which 26 // can be Weak or Strong 27 type Dependency struct { 28 Name string 29 Kind Kind 30 } 31 32 // Dependencies holds a collection of dependencies as a map from a component name 33 // to a list of Dependency instances 34 type Dependencies struct { 35 deps map[string][]Dependency 36 } 37 38 func validDependency(components []string, dependency Dependency) bool { 39 for _, c := range components { 40 if c == dependency.Name { 41 return true 42 } 43 } 44 45 return false 46 } 47 48 func readDependency(line string) (Dependency, error) { 49 dep := strings.TrimRight(strings.TrimSpace(line), "/") 50 51 // blank line or comment 52 if len(dep) < 1 || dep[0] == '#' { 53 return Dependency{}, nil 54 } 55 56 // strong dependency 57 if dep[0] == '!' { 58 return Dependency{dep[1:], Strong}, nil 59 } 60 61 return Dependency{dep, Weak}, nil 62 } 63 64 // ReadManifest reads a single manifest file and returns the dependency list 65 // or validation errors 66 func ReadManifest(path string) (string, []Dependency, []error) { 67 dependencies := make([]Dependency, 0) 68 errors := make([]error, 0) 69 70 file, err := os.Open(path) 71 if err != nil { 72 return "", nil, []error{fmt.Errorf("cannot open dependency manifest %s: %s", path, err)} 73 } 74 75 dir, _ := filepath.Split(path) 76 component := strings.TrimRight(dir, "/") 77 78 scanner := bufio.NewScanner(file) 79 for scanner.Scan() { 80 dep, err := readDependency(scanner.Text()) 81 82 if err != nil { 83 errors = append(errors, err) 84 continue 85 } 86 87 // comment or blank line 88 if dep.Name == "" { 89 continue 90 } 91 92 dependencies = append(dependencies, dep) 93 } 94 95 err = scanner.Err() 96 if err != nil { 97 return "", nil, []error{fmt.Errorf("cannot read dependency manifest %s: %s", path, err)} 98 } 99 100 return component, dependencies, nil 101 } 102 103 // Read manifests at manifestPaths and return a graph of dependencies 104 func Read(manifestPaths []string, dependOnSelf bool) ([]string, Dependencies, []error) { 105 dependencies := make(map[string][]Dependency, len(manifestPaths)) 106 components := make([]string, 0) 107 errors := []error{} 108 109 for _, manifest := range manifestPaths { 110 component, deps, err := ReadManifest(manifest) 111 if err != nil { 112 errors = append(errors, err...) 113 continue 114 } 115 116 if dependOnSelf { 117 deps = append([]Dependency{Dependency{component, Weak}}, deps...) 118 } 119 120 components = append(components, component) 121 dependencies[component] = deps 122 } 123 124 // validate dependencies 125 for manifest, deps := range dependencies { 126 for _, dep := range deps { 127 if !validDependency(components, dep) { 128 errors = append(errors, fmt.Errorf("unknown dependency '%s' of '%s'", dep.Name, manifest)) 129 } 130 } 131 } 132 133 if len(errors) > 0 { 134 return nil, Dependencies{}, errors 135 } 136 137 return components, Dependencies{dependencies}, nil 138 } 139 140 // ReadRepoManifest reads a full repository manifest as produced by monobuild print --full 141 func ReadRepoManifest(manifest string, dependOnSelf bool) ([]string, Dependencies, []error) { 142 lines := strings.Split(manifest, "\n") 143 dependencies := make(map[string][]Dependency, len(lines)) 144 components := make([]string, 0, len(lines)) 145 errors := []error{} 146 147 for _, line := range lines { 148 line = strings.TrimSpace(line) 149 if len(line) < 1 || line[0] == '#' { // skip blank lines and comments 150 continue 151 } 152 153 parts := strings.Split(line, ":") 154 if len(parts) != 2 { 155 errors = append(errors, fmt.Errorf("bad line format: '%s' expected 'componnennt: dependency, dependency, ...'", line)) 156 continue 157 } 158 159 component := strings.TrimSpace(parts[0]) 160 components = append(components, component) 161 162 dependencies[component] = []Dependency{} 163 164 for _, d := range strings.Split(parts[1], ",") { 165 if len(strings.TrimSpace(d)) < 1 { 166 continue 167 } 168 169 dep, err := readDependency(d) 170 if err != nil { 171 errors = append(errors, fmt.Errorf("malformed dependency: %s", d)) 172 continue 173 } 174 175 dependencies[component] = append(dependencies[component], dep) 176 } 177 178 if dependOnSelf { 179 dependencies[component] = append([]Dependency{Dependency{component, Weak}}, dependencies[component]...) 180 } 181 } 182 183 // validate dependencies 184 for manifest, deps := range dependencies { 185 for _, dep := range deps { 186 if !validDependency(components, dep) { 187 errors = append(errors, fmt.Errorf("unknown dependency '%s' of '%s'", dep.Name, manifest)) 188 } 189 } 190 } 191 192 if len(errors) > 0 { 193 return nil, Dependencies{}, errors 194 } 195 196 return components, Dependencies{dependencies}, nil 197 198 } 199 200 // FilterComponents filters a list of files to components 201 func FilterComponents(components []string, changedFiles []string) []string { 202 changedComponents := []string{} 203 204 for _, component := range components { 205 for _, change := range changedFiles { 206 if strings.HasPrefix(change, component+"/") { 207 changedComponents = append(changedComponents, component) 208 break 209 } 210 } 211 } 212 213 return changedComponents 214 } 215 216 // AsGraph returns the dependencies as a graph.Graph 217 func (d Dependencies) AsGraph() graph.Graph { 218 result := make(map[string][]graph.Edge, len(d.deps)) 219 220 for c, ds := range d.deps { 221 result[c] = make([]graph.Edge, 0, len(ds)) 222 223 for _, d := range ds { 224 // FIXME there should really be a mapping of kind to colour 225 result[c] = append(result[c], graph.Edge{Label: d.Name, Colour: int(d.Kind)}) 226 } 227 } 228 229 return graph.New(result) 230 }