Skip to content

Commit

Permalink
sourcebundle: Multi-package module package installation and bundling
Browse files Browse the repository at this point in the history
  • Loading branch information
apparentlymart authored Jun 7, 2023
2 parents 66525e4 + 386bf9b commit 6a7ca7a
Show file tree
Hide file tree
Showing 51 changed files with 4,752 additions and 237 deletions.
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/hashicorp/go-slug

go 1.15
go 1.20

require (
github.com/apparentlymart/go-versions v1.0.1
github.com/google/go-cmp v0.5.9
github.com/hashicorp/terraform-registry-address v0.2.0
github.com/hashicorp/terraform-svchost v0.0.1
golang.org/x/mod v0.10.0
)

require (
github.com/go-test/deep v1.0.3 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/text v0.6.0 // indirect
)
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
github.com/apparentlymart/go-versions v1.0.1 h1:ECIpSn0adcYNsBfSRwdDdz9fWlL+S/6EUd9+irwkBgU=
github.com/apparentlymart/go-versions v1.0.1/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/terraform-registry-address v0.2.0 h1:92LUg03NhfgZv44zpNTLBGIbiyTokQCDcdH5BhVHT3s=
github.com/hashicorp/terraform-registry-address v0.2.0/go.mod h1:478wuzJPzdmqT6OGbB/iH82EDcI8VFM4yujknh/1nIs=
github.com/hashicorp/terraform-svchost v0.0.1 h1:Zj6fR5wnpOHnJUmLyWozjMeDaVuE+cstMPj41/eKmSQ=
github.com/hashicorp/terraform-svchost v0.0.1/go.mod h1:ut8JaH0vumgdCfJaihdcZULqkAwHdQNwNH7taIDdsZM=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
100 changes: 100 additions & 0 deletions internal/ignorefiles/ignorerules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Package ignorefiles deals with the ".terraformignore" file format, which
// is a convention similar to ".gitignore" that specifies path patterns that
// match files Terraform should discard or ignore when interpreting a package
// fetched from a remote location.
package ignorefiles

import (
"fmt"
"io"
"os"
"path/filepath"
)

// A Ruleset is the result of reading, parsing, and compiling a
// ".terraformignore" file.
type Ruleset struct {
rules []rule
}

// ParseIgnoreFileContent takes a reader over the content of a .terraformignore
// file and returns the Ruleset described by that file, or an error if the
// file is invalid.
func ParseIgnoreFileContent(r io.Reader) (*Ruleset, error) {
rules, err := readRules(r)
if err != nil {
return nil, err
}
return &Ruleset{rules: rules}, nil
}

// LoadPackageIgnoreRules implements reasonable default behavior for finding
// ignore rules for a particular package root directory: if .terraformignore is
// present then use it, or otherwise just return DefaultRuleset.
//
// This function will return an error only if an ignore file is present but
// unreadable, or if an ignore file is present but contains invalid syntax.
func LoadPackageIgnoreRules(packageDir string) (*Ruleset, error) {
file, err := os.Open(filepath.Join(packageDir, ".terraformignore"))
if err != nil {
if os.IsNotExist(err) {
return DefaultRuleset, nil
}
return nil, fmt.Errorf("cannot read .terraformignore: %s", err)
}
defer file.Close()

ret, err := ParseIgnoreFileContent(file)
if err != nil {
// The parse errors already mention that they were parsing ignore rules,
// so don't need an additional prefix added.
return nil, err
}
return ret, nil
}

// Excludes tests whether the given path matches the set of paths that are
// excluded by the rules in the ruleset.
//
// If any of the rules in the ruleset have invalid syntax then Excludes will
// return an error, but it will also still return a boolean result which
// considers all of the remaining valid rules, to support callers that want to
// just ignore invalid exclusions. Such callers can safely ignore the error
// result:
//
// exc, _ = ruleset.Excludes(path)
func (r *Ruleset) Excludes(path string) (bool, error) {
if r == nil {
return false, nil
}

var retErr error
foundMatch := false
for _, rule := range r.rules {
match, err := rule.match(path)
if err != nil {
// We'll remember the first error we encounter, but continue
// matching anyway to support callers that want to ignore invalid
// lines and just match with whatever's left.
if retErr == nil {
retErr = fmt.Errorf("invalid ignore rule %q", rule.val)
}
}
if match {
foundMatch = !rule.excluded
}
}
return foundMatch, retErr
}

// Includes is the inverse of [Ruleset.Excludes].
func (r *Ruleset) Includes(path string) (bool, error) {
notRet, err := r.Excludes(path)
return !notRet, err
}

var DefaultRuleset *Ruleset

func init() {
DefaultRuleset = &Ruleset{rules: defaultExclusions}
}
186 changes: 186 additions & 0 deletions internal/ignorefiles/terraformignore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package ignorefiles

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"text/scanner"
)

func readRules(input io.Reader) ([]rule, error) {
rules := defaultExclusions
scanner := bufio.NewScanner(input)
scanner.Split(bufio.ScanLines)

for scanner.Scan() {
pattern := scanner.Text()
// Ignore blank lines
if len(pattern) == 0 {
continue
}
// Trim spaces
pattern = strings.TrimSpace(pattern)
// Ignore comments
if pattern[0] == '#' {
continue
}
// New rule structure
rule := rule{}
// Exclusions
if pattern[0] == '!' {
rule.excluded = true
pattern = pattern[1:]
}
// If it is a directory, add ** so we catch descendants
if pattern[len(pattern)-1] == os.PathSeparator {
pattern = pattern + "**"
}
// If it starts with /, it is absolute
if pattern[0] == os.PathSeparator {
pattern = pattern[1:]
} else {
// Otherwise prepend **/
pattern = "**" + string(os.PathSeparator) + pattern
}
rule.val = pattern
rule.dirs = strings.Split(pattern, string(os.PathSeparator))
rules = append(rules, rule)
}

if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("syntax error in .terraformignore: %w", err)
}
return rules, nil
}

type rule struct {
val string // the value of the rule itself
excluded bool // ! is present, an exclusion rule
dirs []string // directories of the rule
regex *regexp.Regexp // regular expression to match for the rule
}

func (r *rule) match(path string) (bool, error) {
if r.regex == nil {
if err := r.compile(); err != nil {
return false, filepath.ErrBadPattern
}
}

b := r.regex.MatchString(path)
return b, nil
}

func (r *rule) compile() error {
regStr := "^"
pattern := r.val
// Go through the pattern and convert it to a regexp.
// Use a scanner to support utf-8 chars.
var scan scanner.Scanner
scan.Init(strings.NewReader(pattern))

sl := string(os.PathSeparator)
escSL := sl
if sl == `\` {
escSL += `\`
}

for scan.Peek() != scanner.EOF {
ch := scan.Next()
if ch == '*' {
if scan.Peek() == '*' {
// is some flavor of "**"
scan.Next()

// Treat **/ as ** so eat the "/"
if string(scan.Peek()) == sl {
scan.Next()
}

if scan.Peek() == scanner.EOF {
// is "**EOF" - to align with .gitignore just accept all
regStr += ".*"
} else {
// is "**"
// Note that this allows for any # of /'s (even 0) because
// the .* will eat everything, even /'s
regStr += "(.*" + escSL + ")?"
}
} else {
// is "*" so map it to anything but "/"
regStr += "[^" + escSL + "]*"
}
} else if ch == '?' {
// "?" is any char except "/"
regStr += "[^" + escSL + "]"
} else if ch == '.' || ch == '$' {
// Escape some regexp special chars that have no meaning
// in golang's filepath.Match
regStr += `\` + string(ch)
} else if ch == '\\' {
// escape next char. Note that a trailing \ in the pattern
// will be left alone (but need to escape it)
if sl == `\` {
// On windows map "\" to "\\", meaning an escaped backslash,
// and then just continue because filepath.Match on
// Windows doesn't allow escaping at all
regStr += escSL
continue
}
if scan.Peek() != scanner.EOF {
regStr += `\` + string(scan.Next())
} else {
regStr += `\`
}
} else {
regStr += string(ch)
}
}

regStr += "$"
re, err := regexp.Compile(regStr)
if err != nil {
return err
}

r.regex = re
return nil
}

/*
Default rules as they would appear in .terraformignore:
.git/
.terraform/
!.terraform/modules/
*/

var defaultExclusions = []rule{
{
val: strings.Join([]string{"**", ".git", "**"}, string(os.PathSeparator)),
excluded: false,
},
{
val: strings.Join([]string{"**", ".terraform", "**"}, string(os.PathSeparator)),
excluded: false,
},
{
val: strings.Join([]string{"**", ".terraform", "modules", "**"}, string(os.PathSeparator)),
excluded: true,
},
}

func init() {
// We'll precompile all of the default rules at initialization, so we
// don't need to recompile them every time we encounter a package that
// doesn't have any rules (the common case).
for _, r := range defaultExclusions {
err := r.compile()
if err != nil {
panic(fmt.Sprintf("invalid default rule %q: %s", r.val, err))
}
}
}
Loading

0 comments on commit 6a7ca7a

Please sign in to comment.