File: reader.go

package info (click to toggle)
apptainer 1.4.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 12,780 kB
  • sloc: sh: 3,329; ansic: 1,706; awk: 414; python: 103; makefile: 54
file content (97 lines) | stat: -rw-r--r-- 3,154 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// Copyright (c) Contributors to the Apptainer project, established as
//   Apptainer a Series of LF Projects LLC.
//   For website terms of use, trademark policy, privacy policy and other
//   project policies see https://lfprojects.org/policies
// Copyright (c) 2019-2023, Sylabs Inc. All rights reserved.
// Copyright (c) Contributors to the Apptainer project, established as
//   Apptainer a Series of LF Projects LLC.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package args

import (
	"bytes"
	"fmt"
	"io"
	"regexp"
	"strings"

	"github.com/samber/lo"
)

var (
	buildArgsRegexp   = regexp.MustCompile(`{{\s*(\w+)\s*}}`)
	commentLineRegexp = regexp.MustCompile(`\s*[#][^!]\s*.*`)
)

// NewReader creates a io.Reader that will provide the contents of a def file
// with build-args replacements applied. src is an io.Reader from which the
// pre-replacement def file will be read. buildArgsMap provides the replacements
// requested by the user, and defaultArgsMap provides the replacements specified
// in the %arguments section of the def file (or build stage). The arguments
// actually encountered in the course of the replacement will be appended to the
// slice designated by consumedArgs.
func NewReader(src io.Reader, buildArgsMap map[string]string, defaultArgsMap map[string]string, consumedArgs *[]string) (io.Reader, error) {
	srcBytes, err := io.ReadAll(src)
	if err != nil {
		return nil, err
	}

	// do templating
	matches := buildArgsRegexp.FindAllSubmatchIndex(srcBytes, -1)
	mapOfConsumedArgs := make(map[string]bool)
	var buf bytes.Buffer
	bufWriter := io.Writer(&buf)
	i := 0
	for _, m := range matches {
		// find the last newline
		newlineIdx := i
		for start := m[0]; start >= newlineIdx; start-- {
			if srcBytes[start] == '\n' {
				newlineIdx = start
				break
			}
		}

		// check whether current line containing {{ VAR }} is commented line
		if commentLineRegexp.Match(srcBytes[newlineIdx:m[0]]) {
			continue
		}

		bufWriter.Write(srcBytes[i:m[0]])
		argName := string(srcBytes[m[2]:m[3]])
		val, ok := buildArgsMap[argName]
		if !ok {
			val, ok = defaultArgsMap[argName]
		}
		if !ok {
			return nil, fmt.Errorf("build var %s is not defined through either --build-arg (--build-arg-file) or 'arguments' section", argName)
		}

		// before setting the value, we need to handle nested defined variables inside %arguments section
		matches := buildArgsRegexp.FindAllStringSubmatchIndex(val, -1)
		newVal := val
		for _, m := range matches {
			k := val[m[2]:m[3]]
			if v, ok := buildArgsMap[k]; ok {
				// replace the variable with defined value
				newVal = strings.Replace(newVal, val[m[0]:m[1]], v, -1)
			} else if v, ok := defaultArgsMap[k]; ok {
				newVal = strings.Replace(newVal, val[m[0]:m[1]], v, -1)
			}
		}

		bufWriter.Write([]byte(newVal))
		mapOfConsumedArgs[argName] = true
		i = m[1]
	}
	bufWriter.Write(srcBytes[i:])

	*consumedArgs = append(*consumedArgs, lo.Keys(mapOfConsumedArgs)...)

	r := bytes.NewReader(buf.Bytes())

	return r, nil
}