1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #8748 from duglin/Issue8330

Have .dockerignore support Dockerfile/.dockerignore
This commit is contained in:
Michael Crosby 2015-01-06 13:47:42 -08:00
commit 6d780139c4
16 changed files with 271 additions and 84 deletions

View file

@ -14,7 +14,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -30,6 +29,7 @@ import (
"github.com/docker/docker/nat" "github.com/docker/docker/nat"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/pkg/parsers/filters"
@ -140,32 +140,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
if _, err = os.Stat(filename); os.IsNotExist(err) { if _, err = os.Stat(filename); os.IsNotExist(err) {
return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0)) return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0))
} }
var excludes []string var includes []string = []string{"."}
ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore"))
if err != nil && !os.IsNotExist(err) { excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore"))
return fmt.Errorf("Error reading .dockerignore: '%s'", err) if err != nil {
return err
} }
for _, pattern := range strings.Split(string(ignore), "\n") {
pattern = strings.TrimSpace(pattern) // If .dockerignore mentions .dockerignore or Dockerfile
if pattern == "" { // then make sure we send both files over to the daemon
continue // because Dockerfile is, obviously, needed no matter what, and
} // .dockerignore is needed to know if either one needs to be
pattern = filepath.Clean(pattern) // removed. The deamon will remove them for us, if needed, after it
ok, err := filepath.Match(pattern, "Dockerfile") // parses the Dockerfile.
if err != nil { keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
return fmt.Errorf("Bad .dockerignore pattern: '%s', error: %s", pattern, err) keepThem2, _ := fileutils.Matches("Dockerfile", excludes)
} if keepThem1 || keepThem2 {
if ok { includes = append(includes, ".dockerignore", "Dockerfile")
return fmt.Errorf("Dockerfile was excluded by .dockerignore pattern '%s'", pattern)
}
excludes = append(excludes, pattern)
} }
if err = utils.ValidateContextDirectory(root, excludes); err != nil { if err = utils.ValidateContextDirectory(root, excludes); err != nil {
return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err) return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
} }
options := &archive.TarOptions{ options := &archive.TarOptions{
Compression: archive.Uncompressed, Compression: archive.Uncompressed,
Excludes: excludes, ExcludePatterns: excludes,
IncludeFiles: includes,
} }
context, err = archive.TarWithOptions(root, options) context, err = archive.TarWithOptions(root, options)
if err != nil { if err != nil {

View file

@ -31,6 +31,7 @@ import (
"github.com/docker/docker/builder/parser" "github.com/docker/docker/builder/parser"
"github.com/docker/docker/daemon" "github.com/docker/docker/daemon"
"github.com/docker/docker/engine" "github.com/docker/docker/engine"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/pkg/tarsum"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
"github.com/docker/docker/runconfig" "github.com/docker/docker/runconfig"
@ -136,30 +137,10 @@ func (b *Builder) Run(context io.Reader) (string, error) {
} }
}() }()
filename := path.Join(b.contextPath, "Dockerfile") if err := b.readDockerfile("Dockerfile"); err != nil {
fi, err := os.Stat(filename)
if os.IsNotExist(err) {
return "", fmt.Errorf("Cannot build a directory without a Dockerfile")
}
if fi.Size() == 0 {
return "", ErrDockerfileEmpty
}
f, err := os.Open(filename)
if err != nil {
return "", err return "", err
} }
defer f.Close()
ast, err := parser.Parse(f)
if err != nil {
return "", err
}
b.dockerfile = ast
// some initializations that would not have been supplied by the caller. // some initializations that would not have been supplied by the caller.
b.Config = &runconfig.Config{} b.Config = &runconfig.Config{}
b.TmpContainers = map[string]struct{}{} b.TmpContainers = map[string]struct{}{}
@ -185,6 +166,53 @@ func (b *Builder) Run(context io.Reader) (string, error) {
return b.image, nil return b.image, nil
} }
// Reads a Dockerfile from the current context. It assumes that the
// 'filename' is a relative path from the root of the context
func (b *Builder) readDockerfile(filename string) error {
filename = path.Join(b.contextPath, filename)
fi, err := os.Stat(filename)
if os.IsNotExist(err) {
return fmt.Errorf("Cannot build a directory without a Dockerfile")
}
if fi.Size() == 0 {
return ErrDockerfileEmpty
}
f, err := os.Open(filename)
if err != nil {
return err
}
b.dockerfile, err = parser.Parse(f)
f.Close()
if err != nil {
return err
}
// After the Dockerfile has been parsed, we need to check the .dockerignore
// file for either "Dockerfile" or ".dockerignore", and if either are
// present then erase them from the build context. These files should never
// have been sent from the client but we did send them to make sure that
// we had the Dockerfile to actually parse, and then we also need the
// .dockerignore file to know whether either file should be removed.
// Note that this assumes the Dockerfile has been read into memory and
// is now safe to be removed.
excludes, _ := utils.ReadDockerIgnore(path.Join(b.contextPath, ".dockerignore"))
if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true {
os.Remove(path.Join(b.contextPath, ".dockerignore"))
b.context.(tarsum.BuilderContext).Remove(".dockerignore")
}
if rm, _ := fileutils.Matches("Dockerfile", excludes); rm == true {
os.Remove(path.Join(b.contextPath, "Dockerfile"))
b.context.(tarsum.BuilderContext).Remove("Dockerfile")
}
return nil
}
// This method is the entrypoint to all statement handling routines. // This method is the entrypoint to all statement handling routines.
// //
// Almost all nodes will have this structure: // Almost all nodes will have this structure:

View file

@ -390,7 +390,15 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri
for _, fileInfo := range b.context.GetSums() { for _, fileInfo := range b.context.GetSums() {
absFile := path.Join(b.contextPath, fileInfo.Name()) absFile := path.Join(b.contextPath, fileInfo.Name())
if strings.HasPrefix(absFile, absOrigPath) || absFile == absOrigPathNoSlash { // Any file in the context that starts with the given path will be
// picked up and its hashcode used. However, we'll exclude the
// root dir itself. We do this for a coupel of reasons:
// 1 - ADD/COPY will not copy the dir itself, just its children
// so there's no reason to include it in the hash calc
// 2 - the metadata on the dir will change when any child file
// changes. This will lead to a miss in the cache check if that
// child file is in the .dockerignore list.
if strings.HasPrefix(absFile, absOrigPath) && absFile != absOrigPathNoSlash {
subfiles = append(subfiles, fileInfo.Sum()) subfiles = append(subfiles, fileInfo.Sum())
} }
} }

View file

@ -888,8 +888,8 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
} }
archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{ archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{
Compression: archive.Uncompressed, Compression: archive.Uncompressed,
Includes: filter, IncludeFiles: filter,
}) })
if err != nil { if err != nil {
container.Unmount() container.Unmount()

View file

@ -300,8 +300,8 @@ func (a *Driver) Put(id string) {
func (a *Driver) Diff(id, parent string) (archive.Archive, error) { func (a *Driver) Diff(id, parent string) (archive.Archive, error) {
// AUFS doesn't need the parent layer to produce a diff. // AUFS doesn't need the parent layer to produce a diff.
return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{
Compression: archive.Uncompressed, Compression: archive.Uncompressed,
Excludes: []string{".wh..wh.*"}, ExcludePatterns: []string{".wh..wh.*"},
}) })
} }

View file

@ -154,6 +154,12 @@ Exclusion patterns match files or directories relative to the source repository
that will be excluded from the context. Globbing is done using Go's that will be excluded from the context. Globbing is done using Go's
[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. [filepath.Match](http://golang.org/pkg/path/filepath#Match) rules.
> **Note**:
> The `.dockerignore` file can even be used to ignore the `Dockerfile` and
> `.dockerignore` files. This might be useful if you are copying files from
> the root of the build context into your new containter but do not want to
> include the `Dockerfile` or `.dockerignore` files (e.g. `ADD . /someDir/`).
The following example shows the use of the `.dockerignore` file to exclude the The following example shows the use of the `.dockerignore` file to exclude the
`.git` directory from the context. Its effect can be seen in the changed size of `.git` directory from the context. Its effect can be seen in the changed size of
the uploaded context. the uploaded context.

View file

@ -57,7 +57,7 @@ func (s *TagStore) CmdLoad(job *engine.Job) engine.Status {
excludes[i] = k excludes[i] = k
i++ i++
} }
if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil { if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil {
return job.Error(err) return job.Error(err)
} }

View file

@ -3131,28 +3131,106 @@ func TestBuildDockerignoringDockerfile(t *testing.T) {
name := "testbuilddockerignoredockerfile" name := "testbuilddockerignoredockerfile"
defer deleteImages(name) defer deleteImages(name)
dockerfile := ` dockerfile := `
FROM scratch` FROM busybox
ADD . /tmp/
RUN ! ls /tmp/Dockerfile
RUN ls /tmp/.dockerignore`
ctx, err := fakeContext(dockerfile, map[string]string{ ctx, err := fakeContext(dockerfile, map[string]string{
"Dockerfile": "FROM scratch", "Dockerfile": dockerfile,
".dockerignore": "Dockerfile\n", ".dockerignore": "Dockerfile\n",
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer ctx.Close() if _, err = buildImageFromContext(name, ctx, true); err != nil {
if _, err = buildImageFromContext(name, ctx, true); err == nil { t.Fatalf("Didn't ignore Dockerfile correctly:%s", err)
t.Fatalf("Didn't get expected error from ignoring Dockerfile")
} }
// now try it with ./Dockerfile // now try it with ./Dockerfile
ctx.Add(".dockerignore", "./Dockerfile\n") ctx.Add(".dockerignore", "./Dockerfile\n")
if _, err = buildImageFromContext(name, ctx, true); err == nil { if _, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't get expected error from ignoring ./Dockerfile") t.Fatalf("Didn't ignore ./Dockerfile correctly:%s", err)
} }
logDone("build - test .dockerignore of Dockerfile") logDone("build - test .dockerignore of Dockerfile")
} }
func TestBuildDockerignoringDockerignore(t *testing.T) {
name := "testbuilddockerignoredockerignore"
defer deleteImages(name)
dockerfile := `
FROM busybox
ADD . /tmp/
RUN ! ls /tmp/.dockerignore
RUN ls /tmp/Dockerfile`
ctx, err := fakeContext(dockerfile, map[string]string{
"Dockerfile": dockerfile,
".dockerignore": ".dockerignore\n",
})
defer ctx.Close()
if err != nil {
t.Fatal(err)
}
if _, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't ignore .dockerignore correctly:%s", err)
}
logDone("build - test .dockerignore of .dockerignore")
}
func TestBuildDockerignoreTouchDockerfile(t *testing.T) {
var id1 string
var id2 string
name := "testbuilddockerignoretouchdockerfile"
defer deleteImages(name)
dockerfile := `
FROM busybox
ADD . /tmp/`
ctx, err := fakeContext(dockerfile, map[string]string{
"Dockerfile": dockerfile,
".dockerignore": "Dockerfile\n",
})
defer ctx.Close()
if err != nil {
t.Fatal(err)
}
if id1, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't build it correctly:%s", err)
}
if id2, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't build it correctly:%s", err)
}
if id1 != id2 {
t.Fatalf("Didn't use the cache - 1")
}
// Now make sure touching Dockerfile doesn't invalidate the cache
if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil {
t.Fatalf("Didn't add Dockerfile: %s", err)
}
if id2, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't build it correctly:%s", err)
}
if id1 != id2 {
t.Fatalf("Didn't use the cache - 2")
}
// One more time but just 'touch' it instead of changing the content
if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil {
t.Fatalf("Didn't add Dockerfile: %s", err)
}
if id2, err = buildImageFromContext(name, ctx, true); err != nil {
t.Fatalf("Didn't build it correctly:%s", err)
}
if id1 != id2 {
t.Fatalf("Didn't use the cache - 3")
}
logDone("build - test .dockerignore touch dockerfile")
}
func TestBuildDockerignoringWholeDir(t *testing.T) { func TestBuildDockerignoringWholeDir(t *testing.T) {
name := "testbuilddockerignorewholedir" name := "testbuilddockerignorewholedir"
defer deleteImages(name) defer deleteImages(name)

View file

@ -10,14 +10,13 @@ import (
) )
func TestEventsUntag(t *testing.T) { func TestEventsUntag(t *testing.T) {
out, _, _ := dockerCmd(t, "images", "-q") image := "busybox"
image := strings.Split(out, "\n")[0]
dockerCmd(t, "tag", image, "utest:tag1") dockerCmd(t, "tag", image, "utest:tag1")
dockerCmd(t, "tag", image, "utest:tag2") dockerCmd(t, "tag", image, "utest:tag2")
dockerCmd(t, "rmi", "utest:tag1") dockerCmd(t, "rmi", "utest:tag1")
dockerCmd(t, "rmi", "utest:tag2") dockerCmd(t, "rmi", "utest:tag2")
eventsCmd := exec.Command("timeout", "0.2", dockerBinary, "events", "--since=1") eventsCmd := exec.Command("timeout", "0.2", dockerBinary, "events", "--since=1")
out, _, _ = runCommandWithOutput(eventsCmd) out, _, _ := runCommandWithOutput(eventsCmd)
events := strings.Split(out, "\n") events := strings.Split(out, "\n")
nEvents := len(events) nEvents := len(events)
// The last element after the split above will be an empty string, so we // The last element after the split above will be an empty string, so we

View file

@ -30,11 +30,11 @@ type (
ArchiveReader io.Reader ArchiveReader io.Reader
Compression int Compression int
TarOptions struct { TarOptions struct {
Includes []string IncludeFiles []string
Excludes []string ExcludePatterns []string
Compression Compression Compression Compression
NoLchown bool NoLchown bool
Name string Name string
} }
// Archiver allows the reuse of most utility functions of this package // Archiver allows the reuse of most utility functions of this package
@ -378,7 +378,7 @@ func escapeName(name string) string {
} }
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative // TarWithOptions creates an archive from the directory at `path`, only including files whose relative
// paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`. // paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`.
func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
@ -401,12 +401,14 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
// mutating the filesystem and we can see transient errors // mutating the filesystem and we can see transient errors
// from this // from this
if options.Includes == nil { if options.IncludeFiles == nil {
options.Includes = []string{"."} options.IncludeFiles = []string{"."}
} }
seen := make(map[string]bool)
var renamedRelFilePath string // For when tar.Options.Name is set var renamedRelFilePath string // For when tar.Options.Name is set
for _, include := range options.Includes { for _, include := range options.IncludeFiles {
filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
if err != nil { if err != nil {
log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err)
@ -420,10 +422,19 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
return nil return nil
} }
skip, err := fileutils.Matches(relFilePath, options.Excludes) skip := false
if err != nil {
log.Debugf("Error matching %s", relFilePath, err) // If "include" is an exact match for the current file
return err // then even if there's an "excludePatterns" pattern that
// matches it, don't skip it. IOW, assume an explicit 'include'
// is asking for that file no matter what - which is true
// for some files, like .dockerignore and Dockerfile (sometimes)
if include != relFilePath {
skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns)
if err != nil {
log.Debugf("Error matching %s", relFilePath, err)
return err
}
} }
if skip { if skip {
@ -433,6 +444,11 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
return nil return nil
} }
if seen[relFilePath] {
return nil
}
seen[relFilePath] = true
// Rename the base resource // Rename the base resource
if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) {
renamedRelFilePath = relFilePath renamedRelFilePath = relFilePath
@ -487,7 +503,7 @@ loop:
// This keeps "../" as-is, but normalizes "/../" to "/" // This keeps "../" as-is, but normalizes "/../" to "/"
hdr.Name = filepath.Clean(hdr.Name) hdr.Name = filepath.Clean(hdr.Name)
for _, exclude := range options.Excludes { for _, exclude := range options.ExcludePatterns {
if strings.HasPrefix(hdr.Name, exclude) { if strings.HasPrefix(hdr.Name, exclude) {
continue loop continue loop
} }
@ -563,8 +579,8 @@ func Untar(archive io.Reader, dest string, options *TarOptions) error {
if options == nil { if options == nil {
options = &TarOptions{} options = &TarOptions{}
} }
if options.Excludes == nil { if options.ExcludePatterns == nil {
options.Excludes = []string{} options.ExcludePatterns = []string{}
} }
decompressedArchive, err := DecompressStream(archive) decompressedArchive, err := DecompressStream(archive)
if err != nil { if err != nil {

View file

@ -165,8 +165,8 @@ func TestTarUntar(t *testing.T) {
Gzip, Gzip,
} { } {
changes, err := tarUntar(t, origin, &TarOptions{ changes, err := tarUntar(t, origin, &TarOptions{
Compression: c, Compression: c,
Excludes: []string{"3"}, ExcludePatterns: []string{"3"},
}) })
if err != nil { if err != nil {
@ -196,8 +196,8 @@ func TestTarWithOptions(t *testing.T) {
opts *TarOptions opts *TarOptions
numChanges int numChanges int
}{ }{
{&TarOptions{Includes: []string{"1"}}, 1}, {&TarOptions{IncludeFiles: []string{"1"}}, 1},
{&TarOptions{Excludes: []string{"2"}}, 1}, {&TarOptions{ExcludePatterns: []string{"2"}}, 1},
} }
for _, testCase := range cases { for _, testCase := range cases {
changes, err := tarUntar(t, origin, testCase.opts) changes, err := tarUntar(t, origin, testCase.opts)

View file

@ -50,8 +50,8 @@ func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error
if options == nil { if options == nil {
options = &archive.TarOptions{} options = &archive.TarOptions{}
} }
if options.Excludes == nil { if options.ExcludePatterns == nil {
options.Excludes = []string{} options.ExcludePatterns = []string{}
} }
var ( var (

View file

@ -40,7 +40,7 @@ func TestChrootTarUntar(t *testing.T) {
if err := os.MkdirAll(dest, 0700); err != nil { if err := os.MkdirAll(dest, 0700); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := Untar(stream, dest, &archive.TarOptions{Excludes: []string{"lolo"}}); err != nil { if err := Untar(stream, dest, &archive.TarOptions{ExcludePatterns: []string{"lolo"}}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View file

@ -0,0 +1,20 @@
package tarsum
// This interface extends TarSum by adding the Remove method. In general
// there was concern about adding this method to TarSum itself so instead
// it is being added just to "BuilderContext" which will then only be used
// during the .dockerignore file processing - see builder/evaluator.go
type BuilderContext interface {
TarSum
Remove(string)
}
func (bc *tarSum) Remove(filename string) {
for i, fis := range bc.sums {
if fis.Name() == filename {
bc.sums = append(bc.sums[:i], bc.sums[i+1:]...)
// Note, we don't just return because there could be
// more than one with this name
}
}
}

View file

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"bufio"
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"crypto/sha1" "crypto/sha1"
@ -492,3 +493,34 @@ func StringsContainsNoCase(slice []string, s string) bool {
} }
return false return false
} }
// Reads a .dockerignore file and returns the list of file patterns
// to ignore. Note this will trim whitespace from each line as well
// as use GO's "clean" func to get the shortest/cleanest path for each.
func ReadDockerIgnore(path string) ([]string, error) {
// Note that a missing .dockerignore file isn't treated as an error
reader, err := os.Open(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("Error reading '%s': %v", path, err)
}
return nil, nil
}
defer reader.Close()
scanner := bufio.NewScanner(reader)
var excludes []string
for scanner.Scan() {
pattern := strings.TrimSpace(scanner.Text())
if pattern == "" {
continue
}
pattern = filepath.Clean(pattern)
excludes = append(excludes, pattern)
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("Error reading '%s': %v", path, err)
}
return excludes, nil
}

View file

@ -47,9 +47,9 @@ func (v *Volume) Export(resource, name string) (io.ReadCloser, error) {
basePath = path.Dir(basePath) basePath = path.Dir(basePath)
} }
return archive.TarWithOptions(basePath, &archive.TarOptions{ return archive.TarWithOptions(basePath, &archive.TarOptions{
Compression: archive.Uncompressed, Compression: archive.Uncompressed,
Name: name, Name: name,
Includes: filter, IncludeFiles: filter,
}) })
} }