Unexpected `project.toml` include/exclude behavior
Summary
When specifying a list of files to include or exclude in a build in project.toml, the behavior is somewhat surprising when specifying directories (e.g. entries ending with a trailing slash). In particular, an excluded directory foo/ will still be present (albeit empty) during build and in the final image. Conversely, an included, empty foo/ directory will not be present during build not the final image.
The spec only describes the behavior for files, and the current behavior might make sense considering .gitignore patterns are used in both cases. However, it seems reasonable to expect that directory entries (ending in a trailing slash) would also include/exclude the actual directories?
It's possible that this should be handled by the lifecycle instead (if you agree the current behavior is unexpected). Either way the Project Descriptor spec should likely be updated to reflect how directory entries are handled.
Reproduction
Steps
These steps only cover the exclude case.
With an input directory:
$ find .
.
./foo
./foo/bar.baz
./project.toml
And a project.toml:
[_]
schema-version = "0.2"
[io.buildpacks]
exclude = [
"foo/"
]
[[io.buildpacks.group]]
id = "foo/finders"
[io.buildpacks.group.script]
api = "0.10"
inline = "find ."
Current behavior
Running pack build foo returns:
...
===> BUILDING
.
./foo
./project.toml
...
(Showing the now empty excluded directory foo/ is still present during build).
Expected behavior
Running pack build foo should (probably) return:
...
===> BUILDING
.
./project.toml
...
(Excluding both the foo/* files as well as the foo directory itself)
Environment
pack info
Pack:
Version: 0.38.1+git-9bd06a9.build-6494
OS/Arch: darwin/arm64
Default Lifecycle Version: 0.20.8
Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13
...
docker info
Client:
Version: 28.2.2
Context: desktop-linux
Debug Mode: false
...
@runesoerensen Thanks for reporting this issue!
The spec only describes the behavior for files, and the current behavior might make sense considering .gitignore patterns are used in both cases. However, it seems reasonable to expect that directory entries (ending in a trailing slash) would also include/exclude the actual directories?
I think you are right, I would expect the same behavior as well, and the spec should be updated to reflect it.
Not sure, but this could also be related to https://github.com/buildpacks/pack/issues/1419
Key Findings and Analysis
I've investigated this issue and identified the root cause of the unexpected project.toml exclude/include behavior for directories.
Root Cause
The current implementation uses github.com/sabhiram/go-gitignore for pattern matching, which follows .gitignore semantics. However, the file filtering logic in pkg/archive/archive.go:WriteDirToTar() only excludes individual entries when the filter returns false, but doesn't properly handle directory exclusion.
When excluding a directory pattern like "foo/", the filter correctly matches files within that directory (e.g., foo/bar.baz), but the directory entry "foo" itself doesn't get excluded because:
-
filepath.Walkvisits directories before their contents - The current logic only skips the specific entry when
fileFilter(relPath)returns false - For directory patterns with trailing slash, the empty directory still gets created in the tar archive
Proposed Fix
I plan to implement a solution that:
-
Enhances the file filter logic in
pkg/client/build.go:getFileFilter()to properly detect when a directory should be completely excluded -
Modifies
WriteDirToTarinpkg/archive/archive.goto skip directory creation when the directory itself matches an exclusion pattern - Adds comprehensive tests for both file and directory exclusion/inclusion patterns
Implementation Approach
The fix will check if a directory path (when it's a directory) matches any exclude patterns that end with /. If so, it will skip creating the directory entry entirely, ensuring both the directory and its contents are excluded from the final image.
This approach maintains backward compatibility while fixing the unexpected behavior described in the issue.
I'm working on implementing this fix and will submit a PR soon.
🤖 Generated with Claude Code