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 }