github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/modinstaller/mod_installer_require.go (about)

     1  package modinstaller
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  
     8  	"github.com/hashicorp/hcl/v2/hclwrite"
     9  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    10  	"github.com/zclconf/go-cty/cty"
    11  )
    12  
    13  // updates the 'require' block in 'mod.sp'
    14  func (i *ModInstaller) updateModFile() error {
    15  	contents, err := i.loadModFileBytes()
    16  	if err != nil {
    17  		return err
    18  	}
    19  
    20  	oldRequire := i.oldRequire
    21  	newRequire := i.workspaceMod.Require
    22  
    23  	// fill these requires in with empty requires
    24  	// so that we don't have to do nil checks everywhere
    25  	// from here on out - if it's empty - it's nil
    26  
    27  	if oldRequire == nil {
    28  		// use an empty require as the old requirements
    29  		oldRequire = modconfig.NewRequire()
    30  	}
    31  	if newRequire == nil {
    32  		// use a stub require instance
    33  		newRequire = modconfig.NewRequire()
    34  	}
    35  
    36  	changes := EmptyChangeSet()
    37  
    38  	if i.shouldDeleteRequireBlock(oldRequire, newRequire) {
    39  		changes = i.buildChangeSetForRequireDelete(oldRequire, newRequire)
    40  	} else if i.shouldCreateRequireBlock(oldRequire, newRequire) {
    41  		changes = i.buildChangeSetForRequireCreate(oldRequire, newRequire)
    42  	} else if !newRequire.Empty() && !oldRequire.Empty() {
    43  		changes = i.calculateChangeSet(oldRequire, newRequire)
    44  	}
    45  
    46  	if len(changes) == 0 {
    47  		// nothing to do here
    48  		return nil
    49  	}
    50  
    51  	contents.ApplyChanges(changes)
    52  	contents.Apply(hclwrite.Format)
    53  
    54  	return os.WriteFile(i.workspaceMod.FilePath(), contents.Bytes(), 0644)
    55  }
    56  
    57  // loads the contents of the mod.sp file and wraps it with a thin wrapper
    58  // to assist in byte sequence manipulation
    59  func (i *ModInstaller) loadModFileBytes() (*ByteSequence, error) {
    60  	modFileBytes, err := os.ReadFile(i.workspaceMod.FilePath())
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	return NewByteSequence(modFileBytes), nil
    65  }
    66  
    67  func (i *ModInstaller) shouldDeleteRequireBlock(oldRequire *modconfig.Require, newRequire *modconfig.Require) bool {
    68  	return newRequire.Empty() && !oldRequire.Empty()
    69  }
    70  
    71  func (i *ModInstaller) shouldCreateRequireBlock(oldRequire *modconfig.Require, newRequire *modconfig.Require) bool {
    72  	return !newRequire.Empty() && oldRequire.Empty()
    73  }
    74  
    75  func (i *ModInstaller) buildChangeSetForRequireDelete(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
    76  	return NewChangeSet(&Change{
    77  		Operation:   Delete,
    78  		OffsetStart: oldRequire.TypeRange.Start.Byte,
    79  		OffsetEnd:   oldRequire.DeclRange.End.Byte,
    80  	})
    81  }
    82  
    83  func (i *ModInstaller) buildChangeSetForRequireCreate(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
    84  	// if the new require is not empty, but the old one is
    85  	// add a new require block with the new stuff
    86  	// by generating the HCL string that goes in
    87  	f := hclwrite.NewEmptyFile()
    88  
    89  	var body *hclwrite.Body
    90  	var insertOffset int
    91  
    92  	if oldRequire.TypeRange.Start.Byte != 0 {
    93  		// this means that there is a require block
    94  		// but is probably empty
    95  		body = f.Body()
    96  		insertOffset = oldRequire.TypeRange.End.Byte - 1
    97  	} else {
    98  		// we don't have a require block at all
    99  		// let's create one to append to
   100  		body = f.Body().AppendNewBlock("require", nil).Body()
   101  		insertOffset = i.workspaceMod.DeclRange.End.Byte - 1
   102  	}
   103  
   104  	for _, mvc := range newRequire.Mods {
   105  		newBlock := i.createNewModRequireBlock(mvc)
   106  		body.AppendBlock(newBlock)
   107  	}
   108  
   109  	// prefix and suffix with new lines
   110  	// this is so that we can handle empty blocks
   111  	// which do not have newlines
   112  	buffer := bytes.NewBuffer([]byte{'\n'})
   113  	buffer.Write(f.Bytes())
   114  	buffer.WriteByte('\n')
   115  
   116  	return NewChangeSet(&Change{
   117  		Operation:   Insert,
   118  		OffsetStart: insertOffset,
   119  		Content:     buffer.Bytes(),
   120  	})
   121  }
   122  
   123  func (i *ModInstaller) calculateChangeSet(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
   124  	if oldRequire.Empty() && newRequire.Empty() {
   125  		// both are empty
   126  		// nothing to do
   127  		return EmptyChangeSet()
   128  	}
   129  	// calculate the changes
   130  	uninstallChanges := i.calcChangesForUninstall(oldRequire, newRequire)
   131  	installChanges := i.calcChangesForInstall(oldRequire, newRequire)
   132  	updateChanges := i.calcChangesForUpdate(oldRequire, newRequire)
   133  
   134  	return MergeChangeSet(
   135  		uninstallChanges,
   136  		installChanges,
   137  		updateChanges,
   138  	)
   139  }
   140  
   141  // creates a new "mod" block which can be written as part of the "require" block in mod.sp
   142  func (i *ModInstaller) createNewModRequireBlock(modVersion *modconfig.ModVersionConstraint) *hclwrite.Block {
   143  	modRequireBlock := hclwrite.NewBlock("mod", []string{modVersion.Name})
   144  	modRequireBlock.Body().SetAttributeValue("version", cty.StringVal(modVersion.VersionString))
   145  	return modRequireBlock
   146  }
   147  
   148  // calculates changes required in mod.sp to reflect uninstalls
   149  func (i *ModInstaller) calcChangesForUninstall(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
   150  	changes := ChangeSet{}
   151  	for _, requiredMod := range oldRequire.Mods {
   152  		// check if this mod is still a dependency
   153  		if modInNew := newRequire.GetModDependency(requiredMod.Name); modInNew == nil {
   154  			changes = append(changes, &Change{
   155  				Operation:   Delete,
   156  				OffsetStart: requiredMod.DefRange.Start.Byte,
   157  				OffsetEnd:   requiredMod.BodyRange.End.Byte,
   158  			})
   159  		}
   160  	}
   161  	return changes
   162  }
   163  
   164  // calculates changes required in mod.sp to reflect new installs
   165  func (i *ModInstaller) calcChangesForInstall(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
   166  	modsToAdd := []*modconfig.ModVersionConstraint{}
   167  	for _, requiredMod := range newRequire.Mods {
   168  		if modInOld := oldRequire.GetModDependency(requiredMod.Name); modInOld == nil {
   169  			modsToAdd = append(modsToAdd, requiredMod)
   170  		}
   171  	}
   172  
   173  	if len(modsToAdd) == 0 {
   174  		// an empty changeset
   175  		return ChangeSet{}
   176  	}
   177  
   178  	// create the HCL serialization for the mod blocks which needs to be placed
   179  	// in the require block
   180  	f := hclwrite.NewEmptyFile()
   181  	rootBody := f.Body()
   182  	for _, modToAdd := range modsToAdd {
   183  		rootBody.AppendBlock(i.createNewModRequireBlock(modToAdd))
   184  	}
   185  
   186  	return ChangeSet{
   187  		&Change{
   188  			Operation:   Insert,
   189  			OffsetStart: oldRequire.DeclRange.End.Byte - 1,
   190  			Content:     f.Bytes(),
   191  		},
   192  	}
   193  }
   194  
   195  // calculates the changes required in mod.sp to reflect updates
   196  func (i *ModInstaller) calcChangesForUpdate(oldRequire *modconfig.Require, newRequire *modconfig.Require) ChangeSet {
   197  	changes := ChangeSet{}
   198  	for _, requiredMod := range oldRequire.Mods {
   199  		modInUpdated := newRequire.GetModDependency(requiredMod.Name)
   200  		if modInUpdated == nil {
   201  			continue
   202  		}
   203  		if modInUpdated.VersionString != requiredMod.VersionString {
   204  			changes = append(changes, &Change{
   205  				Operation:   Replace,
   206  				OffsetStart: requiredMod.VersionRange.Start.Byte,
   207  				OffsetEnd:   requiredMod.VersionRange.End.Byte,
   208  				Content:     []byte(fmt.Sprintf("version = \"%s\"", modInUpdated.VersionString)),
   209  			})
   210  		}
   211  	}
   212  	return changes
   213  }