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  }