github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bob/nix-builder/nix_builder.go (about)

     1  package nixbuilder
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/benchkram/bob/pkg/envutil"
     7  	"github.com/benchkram/errz"
     8  
     9  	"github.com/benchkram/bob/bob/bobfile"
    10  	"github.com/benchkram/bob/pkg/nix"
    11  	"github.com/benchkram/bob/pkg/usererror"
    12  )
    13  
    14  // NB acts as a wrapper for github.com/benchkram/bob/pkg/nix package
    15  // and is used for building tasks dependencies
    16  type NB struct {
    17  	// cache allows caching the dependency to store path
    18  	cache *nix.Cache
    19  
    20  	// shellCache allows caching of the nix-shell --command='env' output
    21  	shellCache *nix.ShellCache
    22  
    23  	// envStore is filled by NixBuilder with the environment
    24  	// used by tasks.
    25  	envStore envutil.Store
    26  }
    27  
    28  type NixOption func(n *NB)
    29  
    30  func WithCache(cache *nix.Cache) NixOption {
    31  	return func(n *NB) {
    32  		n.cache = cache
    33  	}
    34  }
    35  
    36  func WithShellCache(cache *nix.ShellCache) NixOption {
    37  	return func(n *NB) {
    38  		n.shellCache = cache
    39  	}
    40  }
    41  
    42  func WithEnvironmentStore(store envutil.Store) NixOption {
    43  	return func(n *NB) {
    44  		n.envStore = store
    45  	}
    46  }
    47  
    48  // NewNB instantiates a new Nix builder instance
    49  func New(opts ...NixOption) *NB {
    50  	n := &NB{
    51  		envStore: envutil.NewStore(),
    52  	}
    53  
    54  	for _, opt := range opts {
    55  		if opt == nil {
    56  			continue
    57  		}
    58  		opt(n)
    59  	}
    60  
    61  	return n
    62  }
    63  
    64  func (n *NB) EnvStore() envutil.Store {
    65  	return n.envStore
    66  }
    67  
    68  // BuildNixDependenciesInPipeline collects and builds nix-dependencies for a pipeline starting at taskName.
    69  func (n *NB) BuildNixDependenciesInPipeline(ag *bobfile.Bobfile, taskName string) (err error) {
    70  	defer errz.Recover(&err)
    71  
    72  	if !nix.IsInstalled() {
    73  		return usererror.Wrap(fmt.Errorf("nix is not installed on your system. Get it from %s", nix.DownloadURl()))
    74  	}
    75  
    76  	tasksInPipeline, err := ag.BTasks.CollectTasksInPipeline(taskName)
    77  	errz.Fatal(err)
    78  
    79  	return n.BuildNixDependencies(ag, tasksInPipeline, []string{})
    80  }
    81  
    82  // BuildNixDependencies builds nix dependencies and prepares the affected tasks
    83  // by setting the store paths on each task in the given aggregate.
    84  func (n *NB) BuildNixDependencies(ag *bobfile.Bobfile, buildTasksInPipeline, runTasksInPipeline []string) (err error) {
    85  	defer errz.Recover(&err)
    86  
    87  	if !nix.IsInstalled() {
    88  		return usererror.Wrap(fmt.Errorf("nix is not installed on your system. Get it from %s", nix.DownloadURl()))
    89  	}
    90  
    91  	// Resolve nix storePaths from dependencies
    92  	// and rewrite the affected tasks.
    93  	for _, name := range buildTasksInPipeline {
    94  		t := ag.BTasks[name]
    95  
    96  		// construct used dependencies for this task
    97  		var deps []nix.Dependency
    98  		deps = append(deps, t.Dependencies()...)
    99  		deps = nix.UniqueDeps(deps)
   100  
   101  		t.SetNixpkgs(ag.Nixpkgs)
   102  
   103  		hash, err := nix.HashDependencies(deps)
   104  		errz.Fatal(err)
   105  
   106  		if _, ok := n.envStore[envutil.Hash(hash)]; !ok {
   107  			nixShellEnv, err := n.BuildEnvironment(deps, ag.Nixpkgs)
   108  			errz.Fatal(err)
   109  			n.envStore[envutil.Hash(hash)] = nixShellEnv
   110  		}
   111  		t.SetEnvID(envutil.Hash(hash))
   112  
   113  		ag.BTasks[name] = t
   114  	}
   115  
   116  	// FIXME: environment cache is a workaround...
   117  	// either use envSTore and adapt run tasks to use ist as well
   118  	// or remove run tasks entirely.
   119  	environmentCache := make(map[string][]string)
   120  	for _, name := range runTasksInPipeline {
   121  		t := ag.RTasks[name]
   122  
   123  		// construct used dependencies for this task
   124  		var deps []nix.Dependency
   125  		deps = append(deps, t.Dependencies()...)
   126  		deps = nix.UniqueDeps(deps)
   127  
   128  		t.SetNixpkgs(ag.Nixpkgs)
   129  
   130  		hash, err := nix.HashDependencies(deps)
   131  		errz.Fatal(err)
   132  
   133  		if _, ok := environmentCache[hash]; !ok {
   134  			nixShellEnv, err := n.BuildEnvironment(deps, ag.Nixpkgs)
   135  			errz.Fatal(err)
   136  			environmentCache[hash] = nixShellEnv
   137  		}
   138  		t.SetEnv(envutil.Merge(environmentCache[hash], t.Env()))
   139  
   140  		ag.RTasks[name] = t
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // BuildDependencies builds the list of all nix deps
   147  func (n *NB) BuildDependencies(deps []nix.Dependency) error {
   148  	return nix.BuildDependencies(deps, n.cache)
   149  }
   150  
   151  // BuildEnvironment builds the environment with all nix deps
   152  func (n *NB) BuildEnvironment(deps []nix.Dependency, nixpkgs string) (_ []string, err error) {
   153  	return nix.BuildEnvironment(deps, nixpkgs, n.cache, n.shellCache)
   154  }
   155  
   156  // Clean removes all cached nix dependencies
   157  func (n *NB) Clean() (err error) {
   158  	return n.cache.Clean()
   159  }
   160  
   161  // CleanNixShellCache removes all cached nix-shell --command='env' output
   162  func (n *NB) CleanNixShellCache() (err error) {
   163  	if n.shellCache == nil {
   164  		return nil
   165  	}
   166  	return n.shellCache.Clean()
   167  }