pack icon indicating copy to clipboard operation
pack copied to clipboard

Unexpected `project.toml` include/exclude behavior

Open runesoerensen opened this issue 7 months ago • 3 comments

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 avatar Jun 17 '25 00:06 runesoerensen

@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.

jjbustamante avatar Jun 17 '25 16:06 jjbustamante

Not sure, but this could also be related to https://github.com/buildpacks/pack/issues/1419

jjbustamante avatar Jun 17 '25 16:06 jjbustamante

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:

  1. filepath.Walk visits directories before their contents
  2. The current logic only skips the specific entry when fileFilter(relPath) returns false
  3. 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:

  1. Enhances the file filter logic in pkg/client/build.go:getFileFilter() to properly detect when a directory should be completely excluded
  2. Modifies WriteDirToTar in pkg/archive/archive.go to skip directory creation when the directory itself matches an exclusion pattern
  3. 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

jjbustamante avatar Jun 21 '25 16:06 jjbustamante