srpmproc/pkg/directives/spec_change.go
Louis Abel ced0546d71
Change patch directive to maximum compatible form
The `%patchN` method of performing patches in `%prep` has long been
deprecated and should no longer be used. The form of `%patch -PN` is
preferred for maximum compatibility between all current and upcoming
EL releases.
2024-06-24 00:14:23 -07:00

497 lines
14 KiB
Go

// Copyright (c) 2021 The Srpmproc Authors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package directives
import (
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/go-git/go-git/v5"
srpmprocpb "github.com/rocky-linux/srpmproc/pb"
"github.com/rocky-linux/srpmproc/pkg/data"
)
const (
sectionChangelog = "%changelog"
)
var sections = []string{"%description", "%prep", "%build", "%install", "%files", "%changelog"}
type sourcePatchOperationInLoopRequest struct {
cfg *srpmprocpb.Cfg
field string
value *string
longestField int
lastNum *int
in *string
expectedField string
operation srpmprocpb.SpecChange_FileOperation_Type
}
type sourcePatchOperationAfterLoopRequest struct {
cfg *srpmprocpb.Cfg
inLoopNum int
lastNum *int
longestField int
newLines *[]string
in *string
expectedField string
operation srpmprocpb.SpecChange_FileOperation_Type
}
func sourcePatchOperationInLoop(req *sourcePatchOperationInLoopRequest) error {
if strings.HasPrefix(req.field, req.expectedField) {
for _, file := range req.cfg.SpecChange.File {
if file.Type != req.operation {
continue
}
switch file.Mode.(type) {
case *srpmprocpb.SpecChange_FileOperation_Delete:
if file.Name == *req.value {
*req.value = ""
}
break
}
}
if req.field != req.expectedField {
sourceNum, err := strconv.Atoi(strings.Split(req.field, req.expectedField)[1])
if err != nil {
return errors.New(fmt.Sprintf("INVALID_%s_NUM:%s", strings.ToUpper(req.expectedField), req.field))
}
*req.lastNum = sourceNum
}
}
return nil
}
func sourcePatchOperationAfterLoop(req *sourcePatchOperationAfterLoopRequest) (bool, error) {
if req.inLoopNum == *req.lastNum && *req.in == req.expectedField {
for _, file := range req.cfg.SpecChange.File {
if file.Type != req.operation {
continue
}
switch file.Mode.(type) {
case *srpmprocpb.SpecChange_FileOperation_Add:
fieldNum := *req.lastNum + 1
field := fmt.Sprintf("%s%d", req.expectedField, fieldNum)
spaces := calculateSpaces(req.longestField, len(field), req.cfg.SpecChange.DisableAutoAlign)
*req.newLines = append(*req.newLines, fmt.Sprintf("%s:%s%s", field, spaces, file.Name))
if req.expectedField == "Patch" && file.AddToPrep {
val := fmt.Sprintf("%%patch -P%d", fieldNum)
if file.NPath > 0 {
val = fmt.Sprintf("%s -p%d", val, file.NPath)
}
req.cfg.SpecChange.Append = append(req.cfg.SpecChange.Append, &srpmprocpb.SpecChange_AppendOperation{
Field: "%prep",
Value: val,
})
}
*req.lastNum++
break
}
}
*req.in = ""
return true, nil
}
return false, nil
}
func calculateSpaces(longestField int, fieldLength int, disableAutoAlign bool) string {
if disableAutoAlign {
return " "
}
return strings.Repeat(" ", longestField+8-fieldLength)
}
func searchAndReplaceLine(line string, sar []*srpmprocpb.SpecChange_SearchAndReplaceOperation) string {
for _, searchAndReplace := range sar {
switch searchAndReplace.Identifier.(type) {
case *srpmprocpb.SpecChange_SearchAndReplaceOperation_Any:
line = strings.Replace(line, searchAndReplace.Find, searchAndReplace.Replace, int(searchAndReplace.N))
break
case *srpmprocpb.SpecChange_SearchAndReplaceOperation_StartsWith:
if strings.HasPrefix(strings.TrimSpace(line), searchAndReplace.Find) {
line = strings.Replace(line, searchAndReplace.Find, searchAndReplace.Replace, int(searchAndReplace.N))
}
break
case *srpmprocpb.SpecChange_SearchAndReplaceOperation_EndsWith:
if strings.HasSuffix(strings.TrimSpace(line), searchAndReplace.Find) {
line = strings.Replace(line, searchAndReplace.Find, searchAndReplace.Replace, int(searchAndReplace.N))
}
break
}
}
return line
}
func isNextLineSection(lineNum int, lines []string) bool {
if len(lines)-1 > lineNum {
if strings.HasPrefix(strings.TrimSpace(lines[lineNum+1]), "%") {
return true
}
return false
}
return true
}
func setFASlice(futureAdditions map[int][]string, key int, addition string) {
if futureAdditions[key] == nil {
futureAdditions[key] = []string{}
}
futureAdditions[key] = append(futureAdditions[key], addition)
}
func strSliceContains(slice []string, str string) bool {
for _, x := range slice {
if str == x {
return true
}
}
return false
}
func specChange(cfg *srpmprocpb.Cfg, pd *data.ProcessData, md *data.ModeData, _ *git.Worktree, pushTree *git.Worktree) error {
// no spec change operations present
// skip parsing spec
if cfg.SpecChange == nil {
return nil
}
specFiles, err := pushTree.Filesystem.ReadDir("SPECS")
if err != nil {
return errors.New("COULD_NOT_READ_SPECS_DIR")
}
if len(specFiles) != 1 {
return errors.New("ONLY_ONE_SPEC_FILE_IS_SUPPORTED")
}
filePath := filepath.Join("SPECS", specFiles[0].Name())
stat, err := pushTree.Filesystem.Stat(filePath)
if err != nil {
return errors.New("COULD_NOT_STAT_SPEC_FILE")
}
specFile, err := pushTree.Filesystem.OpenFile(filePath, os.O_RDONLY, 0o644)
if err != nil {
return errors.New("COULD_NOT_READ_SPEC_FILE")
}
specBts, err := io.ReadAll(specFile)
if err != nil {
return errors.New("COULD_NOT_READ_ALL_BYTES")
}
specStr := string(specBts)
lines := strings.Split(specStr, "\n")
var newLines []string
futureAdditions := map[int][]string{}
newFieldMemory := map[string]map[string]int{}
lastSourceNum := 0
lastPatchNum := 0
inSection := ""
inField := ""
lastSource := ""
lastPatch := ""
hasPatch := false
version := ""
importName := strings.Replace(pd.Importer.ImportName(pd, md), md.Name, "1", 1)
importNameSplit := strings.SplitN(importName, "-", 2)
if len(importNameSplit) == 2 {
versionSplit := strings.SplitN(importNameSplit[1], ".el", 2)
if len(versionSplit) == 2 {
version = versionSplit[0]
} else {
versionSplit := strings.SplitN(importNameSplit[1], ".module", 2)
if len(versionSplit) == 2 {
version = versionSplit[0]
}
}
}
fieldValueRegex := regexp.MustCompile("^[a-zA-Z0-9]+:")
longestField := 0
for lineNum, line := range lines {
if fieldValueRegex.MatchString(line) {
fieldValue := strings.SplitN(line, ":", 2)
field := strings.TrimSpace(fieldValue[0])
longestField = int(math.Max(float64(len(field)), float64(longestField)))
if strings.HasPrefix(field, "Source") {
lastSource = field
} else if strings.HasPrefix(field, "Patch") {
lastPatch = field
hasPatch = true
} else {
for _, nf := range cfg.SpecChange.NewField {
if field == nf.Key {
if newFieldMemory[field] == nil {
newFieldMemory[field] = map[string]int{}
}
newFieldMemory[field][nf.Value] = lineNum
}
}
}
}
}
for _, nf := range cfg.SpecChange.NewField {
if newFieldMemory[nf.Key] == nil {
newFieldMemory[nf.Key] = map[string]int{}
newFieldMemory[nf.Key][nf.Value] = 0
}
}
for field, nfm := range newFieldMemory {
for value, lineNum := range nfm {
if lineNum != 0 {
newLine := fmt.Sprintf("%s:%s%s", field, calculateSpaces(longestField, len(field), cfg.SpecChange.DisableAutoAlign), value)
setFASlice(futureAdditions, lineNum+1, newLine)
}
}
}
for lineNum, line := range lines {
inLoopSourceNum := lastSourceNum
inLoopPatchNum := lastPatchNum
prefixLine := strings.TrimSpace(line)
for i, additions := range futureAdditions {
if lineNum == i {
for _, addition := range additions {
newLines = append(newLines, addition)
}
}
}
if fieldValueRegex.MatchString(line) {
line = searchAndReplaceLine(line, cfg.SpecChange.SearchAndReplace)
fieldValue := strings.SplitN(line, ":", 2)
field := strings.TrimSpace(fieldValue[0])
value := strings.TrimSpace(fieldValue[1])
if field == lastSource {
inField = "Source"
} else if field == lastPatch {
inField = "Patch"
}
if field == "Version" && version == "" {
version = value
}
for _, searchAndReplace := range cfg.SpecChange.SearchAndReplace {
switch identifier := searchAndReplace.Identifier.(type) {
case *srpmprocpb.SpecChange_SearchAndReplaceOperation_Field:
if field == identifier.Field {
value = strings.Replace(value, searchAndReplace.Find, searchAndReplace.Replace, int(searchAndReplace.N))
}
break
}
}
for _, appendOp := range cfg.SpecChange.Append {
if field == appendOp.Field {
value = value + appendOp.Value
if field == "Release" {
version = version + appendOp.Value
}
}
}
spaces := calculateSpaces(longestField, len(field), cfg.SpecChange.DisableAutoAlign)
err := sourcePatchOperationInLoop(&sourcePatchOperationInLoopRequest{
cfg: cfg,
field: field,
value: &value,
lastNum: &lastSourceNum,
longestField: longestField,
in: &inField,
expectedField: "Source",
operation: srpmprocpb.SpecChange_FileOperation_Source,
})
if err != nil {
return err
}
err = sourcePatchOperationInLoop(&sourcePatchOperationInLoopRequest{
cfg: cfg,
field: field,
value: &value,
longestField: longestField,
lastNum: &lastPatchNum,
in: &inField,
expectedField: "Patch",
operation: srpmprocpb.SpecChange_FileOperation_Patch,
})
if err != nil {
return err
}
if value != "" {
newLines = append(newLines, fmt.Sprintf("%s:%s%s", field, spaces, value))
}
} else {
executed, err := sourcePatchOperationAfterLoop(&sourcePatchOperationAfterLoopRequest{
cfg: cfg,
inLoopNum: inLoopSourceNum,
lastNum: &lastSourceNum,
longestField: longestField,
newLines: &newLines,
expectedField: "Source",
in: &inField,
operation: srpmprocpb.SpecChange_FileOperation_Source,
})
if err != nil {
return err
}
if executed && !hasPatch {
newLines = append(newLines, "")
inField = "Patch"
}
executed, err = sourcePatchOperationAfterLoop(&sourcePatchOperationAfterLoopRequest{
cfg: cfg,
inLoopNum: inLoopPatchNum,
lastNum: &lastPatchNum,
longestField: longestField,
newLines: &newLines,
expectedField: "Patch",
in: &inField,
operation: srpmprocpb.SpecChange_FileOperation_Patch,
})
if err != nil {
return err
}
if executed {
var innerNewLines []string
for field, nfm := range newFieldMemory {
for value, ln := range nfm {
newLine := fmt.Sprintf("%s:%s%s", field, calculateSpaces(longestField, len(field), cfg.SpecChange.DisableAutoAlign), value)
if ln == 0 {
if isNextLineSection(lineNum, lines) {
innerNewLines = append(innerNewLines, newLine)
}
}
}
}
if len(innerNewLines) > 0 {
newLines = append(newLines, "")
for _, il := range innerNewLines {
newLines = append(newLines, il)
}
}
}
if executed && !strings.Contains(specStr, "%changelog") {
newLines = append(newLines, "")
newLines = append(newLines, "%changelog")
inSection = sectionChangelog
}
if inSection == sectionChangelog {
now := time.Now().Format("Mon Jan 02 2006")
for _, changelog := range cfg.SpecChange.Changelog {
newLines = append(newLines, fmt.Sprintf("* %s %s <%s> - %s", now, changelog.AuthorName, changelog.AuthorEmail, version))
for _, msg := range changelog.Message {
newLines = append(newLines, fmt.Sprintf("- %s", msg))
}
newLines = append(newLines, "")
}
inSection = ""
} else {
line = searchAndReplaceLine(line, cfg.SpecChange.SearchAndReplace)
}
if strings.HasPrefix(prefixLine, "%") {
inSection = prefixLine
for _, appendOp := range cfg.SpecChange.Append {
if inSection == appendOp.Field {
insertedLine := 0
for i, x := range lines[lineNum+1:] {
if strSliceContains(sections, strings.TrimSpace(x)) {
insertedLine = lineNum + i
setFASlice(futureAdditions, insertedLine, appendOp.Value)
break
}
}
if insertedLine == 0 {
for i, x := range lines[lineNum+1:] {
if strings.TrimSpace(x) == "" {
insertedLine = lineNum + i + 2
setFASlice(futureAdditions, insertedLine, appendOp.Value)
break
}
}
}
}
}
}
newLines = append(newLines, line)
}
}
err = pushTree.Filesystem.Remove(filePath)
if err != nil {
return errors.New(fmt.Sprintf("COULD_NOT_REMOVE_OLD_SPEC_FILE:%s", filePath))
}
f, err := pushTree.Filesystem.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, stat.Mode())
if err != nil {
return errors.New(fmt.Sprintf("COULD_NOT_OPEN_REPLACEMENT_SPEC_FILE:%s", filePath))
}
_, err = f.Write([]byte(strings.Join(newLines, "\n")))
if err != nil {
return errors.New("COULD_NOT_WRITE_NEW_SPEC_FILE")
}
return nil
}