Skip to content

.zpackignore

A .zpackignore at the root of the directory being packed lists what to leave out. The ignore file itself is never packed - it describes how to build the archive, not what the archive holds.

assets/.zpackignore
# vcs and dependencies
.git/
node_modules/
# build output, at the root only
/build/
# art sources, not shipped
*.psd
# os junk, at any depth
.DS_Store
Thumbs.db
Pattern Matches
Thumbs.db That basename, at any depth
/build build at the archive root only
src/gen Anchored, because the pattern contains a /
build/ Directories only, never a file of the same name
*.psd Any run of characters within one path segment
level-?.json Exactly one character
a/**/tmp Any number of path segments
!keep.png Un-ignores, undoing rules written above it
# note A comment. Blank lines are skipped too

Trailing whitespace is trimmed, so a stray space after a pattern does not change what it matches.

A pattern is anchored - matched against the whole path - if it contains a / anywhere. Otherwise it is matched against the basename only, at any depth.

A leading / anchors without being part of the pattern:

/build/ # excludes build/ at the root
build/ # excludes any directory named build, at any depth

Given this tree:

  • Directorybuild/
    • out.o
  • Directorysrc/
    • Directorybuild/
      • gen.c
    • Thumbs.db
  • Thumbs.db

/build/ excludes build/out.o but keeps src/build/gen.c. Unanchored Thumbs.db excludes both copies.

* and ? stop at a /, so they never cross a directory boundary. ** crosses segments, but only when it stands as a whole segment of its own - a**b behaves as a*b, matching git.

*.psd # art/hero.psd, excluded. art/hero.png, kept.
level-?.json # level-1.json, excluded. level-22.json, kept.
a/**/tmp/ # a/x/y/tmp/, excluded at any depth

Rules apply in order and the last one to match a path decides. A ! rule therefore only undoes rules written above it:

keep/*.png
!keep/keep.png # keep/drop.png excluded, keep/keep.png kept

Reversing those two lines keeps nothing: the broader rule would match last.

A directory that matches is never descended into. .git/ costs nothing to skip regardless of how large it is.

The consequence is worth stating plainly, because it surprises people:

.gitignore zpack
Character classes, [a-z] Not supported - a literal [ matches a literal [
Nested ignore files in subdirectories Not read - only the one at the root of the packed directory
.gitignore files inside subdirectories are additive zpack has exactly one ignore file per archive
Unlimited rules Capped at 1024; beyond that, pack fails with TooManyRules

The rule cap bounds how much work matching one path can cost. If you are near it, the patterns are almost certainly collapsible.

Three things never appear in an archive, no ignore file required:

  1. .zpackignore itself
  2. A file whose relative path equals the output archive path, which is what makes zpack pack . out.zpak repeatable
  3. The temporary file pack builds into, created after the scan so it cannot appear in its own archive

The pattern engine is public API, so a build script can reuse the same rules:

var ignore = try zpack.Ignore.load(gpa, io, dir, ".zpackignore");
defer ignore.deinit();
if (ignore.match("art/hero.psd", false)) {
// excluded
}

See the Ignore reference.