Skip to content

Commit 19aca8b

Browse files
committed
pack: support .packignore
In some cases, there are files that may be useful, but should not be part of artifact. The ability to add files and directories to the .packignore file has been added, which allows you to ignore these files and directories when packing. Closes #812 @TarantoolBot document Title: `tt pack` support `.packignore` Use `.packignore` in the same way as `.gitignore` allows to exclude unnecessary files while preparing application package with `tt pack command.
1 parent 7e746c4 commit 19aca8b

5 files changed

Lines changed: 918 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- `tt pack `: added TCM file packaging.
13+
- `tt pack `: support `.packignore` file to specify files that should not be included
14+
in package (works the same as `.gitignore`).
1315

1416
### Changed
1517

cli/pack/common.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package pack
22

33
import (
4+
"errors"
45
"fmt"
56
"io/fs"
67
"os"
@@ -33,6 +34,8 @@ const (
3334
versionLuaFileName = "VERSION.lua"
3435

3536
rocksManifestPath = ".rocks/share/tarantool/rocks/manifest"
37+
38+
ignoreFile = ".packignore"
3639
)
3740

3841
var (
@@ -51,6 +54,8 @@ var (
5154
}
5255
)
5356

57+
type skipFilter func(srcInfo os.FileInfo, src string) bool
58+
5459
type RocksVersions map[string][]string
5560

5661
// packFileInfo contains information to set for files/dirs in rpm/deb packages.
@@ -76,9 +81,8 @@ func skipDefaults(srcInfo os.FileInfo, src string) bool {
7681
}
7782

7883
// appArtifactsFilters returns a slice of skip functions to avoid copying application artifacts.
79-
func appArtifactsFilters(cliOpts *config.CliOpts, srcAppPath string) []func(
80-
srcInfo os.FileInfo, src string) bool {
81-
filters := make([]func(srcInfo os.FileInfo, src string) bool, 0)
84+
func appArtifactsFilters(cliOpts *config.CliOpts, srcAppPath string) []skipFilter {
85+
filters := make([]skipFilter, 0)
8286
if cliOpts.App == nil {
8387
return filters
8488
}
@@ -102,9 +106,8 @@ func appArtifactsFilters(cliOpts *config.CliOpts, srcAppPath string) []func(
102106
}
103107

104108
// ttEnvironmentFilters prepares a slice of filters for tt environment directories/files.
105-
func ttEnvironmentFilters(packCtx *PackCtx, cliOpts *config.CliOpts) []func(
106-
srcInfo os.FileInfo, src string) bool {
107-
filters := make([]func(srcInfo os.FileInfo, src string) bool, 0)
109+
func ttEnvironmentFilters(packCtx *PackCtx, cliOpts *config.CliOpts) []skipFilter {
110+
filters := make([]skipFilter, 0)
108111
if cliOpts == nil {
109112
return filters
110113
}
@@ -139,10 +142,9 @@ func ttEnvironmentFilters(packCtx *PackCtx, cliOpts *config.CliOpts) []func(
139142
}
140143

141144
// previousPackageFilters returns filters for the previously built packages.
142-
func previousPackageFilters(packCtx *PackCtx) []func(
143-
srcInfo os.FileInfo, src string) bool {
145+
func previousPackageFilters(packCtx *PackCtx) []skipFilter {
144146
pkgName := packCtx.Name
145-
return []func(srcInfo os.FileInfo, src string) bool{
147+
return []skipFilter{
146148
func(srcInfo os.FileInfo, src string) bool {
147149
name := srcInfo.Name()
148150
if strings.HasPrefix(name, pkgName) {
@@ -159,13 +161,18 @@ func previousPackageFilters(packCtx *PackCtx) []func(
159161

160162
// appSrcCopySkip returns a filter func to filter out artifacts paths.
161163
func appSrcCopySkip(packCtx *PackCtx, cliOpts *config.CliOpts,
162-
srcAppPath string) func(srcinfo os.FileInfo, src, dest string) (bool, error) {
164+
srcAppPath string) (func(srcinfo os.FileInfo, src, dest string) (bool, error), error) {
163165
appCopyFilters := appArtifactsFilters(cliOpts, srcAppPath)
164166
appCopyFilters = append(appCopyFilters, ttEnvironmentFilters(packCtx, cliOpts)...)
165167
appCopyFilters = append(appCopyFilters, previousPackageFilters(packCtx)...)
166168
appCopyFilters = append(appCopyFilters, func(srcInfo os.FileInfo, src string) bool {
167169
return skipDefaults(srcInfo, src)
168170
})
171+
if f, err := ignoreFilter(util.GetOsFS(), filepath.Join(srcAppPath, ignoreFile)); err == nil {
172+
appCopyFilters = append(appCopyFilters, f)
173+
} else if !errors.Is(err, fs.ErrNotExist) {
174+
return nil, fmt.Errorf("failed to load %q: %w", ignoreFile, err)
175+
}
169176

170177
return func(srcinfo os.FileInfo, src, dest string) (bool, error) {
171178
for _, shouldSkip := range appCopyFilters {
@@ -174,7 +181,7 @@ func appSrcCopySkip(packCtx *PackCtx, cliOpts *config.CliOpts,
174181
}
175182
}
176183
return false, nil
177-
}
184+
}, nil
178185
}
179186

180187
// getAppNamesToPack generates application names list to pack.
@@ -430,7 +437,10 @@ func copyAppSrc(packCtx *PackCtx, cliOpts *config.CliOpts, srcAppPath, dstAppPat
430437
return err
431438
}
432439

433-
skipFunc := appSrcCopySkip(packCtx, cliOpts, resolvedAppPath)
440+
skipFunc, err := appSrcCopySkip(packCtx, cliOpts, resolvedAppPath)
441+
if err != nil {
442+
return err
443+
}
434444

435445
// Copying application.
436446
log.Debugf("Copying application source %q -> %q", resolvedAppPath, dstAppPath)

cli/pack/ignore.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package pack
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"slices"
12+
"strings"
13+
)
14+
15+
type ignorePattern struct {
16+
re *regexp.Regexp
17+
dirOnly bool
18+
isNegate bool
19+
}
20+
21+
func createIgnorePattern(pattern string, basepath string) (ignorePattern, error) {
22+
var p ignorePattern
23+
var err error
24+
25+
pattern, p.isNegate = strings.CutPrefix(pattern, "!")
26+
if !p.isNegate && (strings.HasPrefix(pattern, "\\!") || strings.HasPrefix(pattern, "\\#")) {
27+
pattern = pattern[1:]
28+
}
29+
pattern, p.dirOnly = strings.CutSuffix(pattern, "/")
30+
if !p.dirOnly && !strings.HasSuffix(pattern, "\\ ") {
31+
pattern = strings.TrimRight(pattern, " ")
32+
}
33+
34+
// Turn pattern to regex expression.
35+
expr := pattern
36+
// First, escape symbols that designate themselves in pattern, but have
37+
// special meaning in regex.
38+
for _, s := range []string{"(", ")", "{", "}", "+"} {
39+
expr = strings.ReplaceAll(expr, s, "\\"+s)
40+
}
41+
// Then replace wildcards with the corresponding regex representation.
42+
// Note that '{0,}' (not '*') is used while replacing '**' to avoid confusing
43+
// in the subsequent replacement of a single '*'.
44+
expr, found := strings.CutPrefix(expr, "**/")
45+
if found {
46+
expr = "([^/]+/){0,}" + expr
47+
}
48+
expr, found = strings.CutSuffix(expr, "/**")
49+
if found {
50+
expr = expr + "(/[^/]+){0,}"
51+
}
52+
expr = strings.ReplaceAll(expr, "/**/", "/([^/]+/){0,}")
53+
expr = strings.ReplaceAll(expr, "\\*", fmt.Sprintf("\\x%x", "*"[0]))
54+
expr = strings.ReplaceAll(expr, "*", "[^/]*")
55+
expr = strings.ReplaceAll(expr, "\\?", fmt.Sprintf("\\x%x", "?"[0]))
56+
expr = strings.ReplaceAll(expr, "?", "[^/]")
57+
58+
if strings.Contains(pattern, "/") {
59+
expr = basepath + expr
60+
} else {
61+
expr = basepath + "([^/]+/)*" + expr
62+
}
63+
64+
p.re, err = regexp.Compile("^" + expr + "$")
65+
if err != nil {
66+
return ignorePattern{}, fmt.Errorf("failed to compile expression: %w", err)
67+
}
68+
69+
return p, nil
70+
}
71+
72+
// loadIgnorePatterns reads ignore patterns from the patternsFile.
73+
func loadIgnorePatterns(fsys fs.FS, patternsFile string) ([]ignorePattern, error) {
74+
contents, err := fs.ReadFile(fsys, patternsFile)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
basepath, _ := filepath.Split(ignoreFile)
80+
81+
var patterns []ignorePattern
82+
s := bufio.NewScanner(bytes.NewReader(contents))
83+
for s.Scan() {
84+
pattern := strings.TrimSpace(s.Text())
85+
if pattern == "" || strings.HasPrefix(pattern, "#") {
86+
continue
87+
}
88+
89+
p, err := createIgnorePattern(pattern, basepath)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
patterns = append(patterns, p)
95+
}
96+
return patterns, nil
97+
}
98+
99+
// ignoreFilter returns filter function that implements .gitignore approach of filtering files.
100+
func ignoreFilter(fsys fs.FS, patternsFile string) (skipFilter, error) {
101+
patterns, err := loadIgnorePatterns(fsys, patternsFile)
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
// According to .gitignore documentation "the last matching pattern decides the outcome"
107+
// so we need to iterate in reverse order until the first match.
108+
slices.Reverse(patterns)
109+
110+
return func(srcInfo os.FileInfo, src string) bool {
111+
// Skip ignore file itself.
112+
if src == patternsFile {
113+
return true
114+
}
115+
for _, p := range patterns {
116+
isApplicable := srcInfo.IsDir() || !p.dirOnly
117+
if isApplicable && p.re.MatchString(src) {
118+
return !p.isNegate
119+
}
120+
}
121+
return false
122+
}, nil
123+
}

0 commit comments

Comments
 (0)