Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ All extensions have been vetted and approved by the Tilt team.
- [`dotenv`](/dotenv): Load environment variables from `.env` or another file.
- [`earthly`](/earthly): Build container images using [earthly](https://earthly.dev)
- [`execute_in_pod`](/execute_in_pod): Execute a command on a pod container.
- [`files_glob`](/files_glob): Expand glob-like patterns into real file paths for use in deps and other APIs.
- [`file_sync_only`](/file_sync_only): No-build, no-push, file sync-only development. Useful when you want to live-reload a single config file into an existing public image, like nginx.
- [`get_obj`](/get_obj): Get object yaml and the container's registry and image from an existing k8s resource such as deployment or statefulset
- [`git_resource`](/git_resource): Deploy a dockerfile from a remote repository -- or specify the path to a local checkout for local development.
Expand Down
44 changes: 44 additions & 0 deletions files_glob/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# files_glob

Author: Kim Eik

Expand glob-like patterns (e.g., `**/*.go`, `dir/*.templ`) into real file paths at Tiltfile load time for use in APIs that require concrete paths (like `deps=` in `docker_build`/`custom_build`).

It also ensures Tilt reloads when matching files are added by watching the base directories implied by the patterns.

## Functions

- files_glob(*patterns) -> [str]
- Expands common glob-style patterns into a de-duplicated list of file paths.
- Examples:
- `dir/**/*.ext` → recursively find files under `dir` matching `*.ext`
- `dir/*.ext` → non-recursive match within `dir`
- `*.ext` → match in current directory only
- literal paths → included as-is
- watch_glob(*patterns)
- Adds `watch_file()` entries for base directories implied by patterns so Tilt reloads when new files appear. `files_glob()` calls this for you automatically.

Note: Implementation uses `bash`/`find` under the hood; it’s meant to be practical for common dev workflows, not a full glob engine.

## Usage

```python path=null start=null
# Optionally set your default extension repo first if not using the shared repo:
# v1alpha1.extension_repo(name='default', url='file:///path/to/tilt-extensions')

load('ext://files_glob', 'files_glob')

# Use with docker_build/custom_build deps
srcs = files_glob('**/*.go', 'web/*.templ', 'scripts/*.sh')

docker_build('myimg', '.', deps=srcs)
```

You can also import `watch_glob` explicitly if you want to add watches without expanding files:

```python path=null start=null
load('ext://files_glob', 'watch_glob')
watch_glob('**/*.sql', 'migrations/**/up/*.sql')
```


106 changes: 106 additions & 0 deletions files_glob/Tiltfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# -*- mode: Python -*-

# Local Tilt extension: expand glob-like patterns into real file paths for use in deps
# Note: deps only accepts real paths; this helper uses bash/find to resolve common
# patterns at Tiltfile load time (e.g., '**/*.ext', 'dir/*.ext', '*.ext').


def _base_dir_from_pattern(pat):
# Determine a base directory from a pattern for watch_file
# Treat patterns that start with '**/' (or are '**' variants) as repo root
if pat.startswith('**/') or pat == '**' or pat == '**/*' or pat.startswith('./**/'):
return '.'
# Handle patterns ending with '/**' or '/**/*' -> base dir before the **
if pat.endswith('/**') or pat.endswith('/**/*'):
base = pat.rsplit('/**', 1)[0]
return base if base else '.'
# Handle recursive patterns with '/**/' inside
if '/**/' in pat:
base = pat.split('/**/', 1)[0]
return base if base else '.'
# For simple 'dir/*.ext' or 'dir/name'
if '/' in pat:
base = pat.rsplit('/', 1)[0]
return base if base else '.'
# For '*.ext' or literal file name, base is current dir
return '.'


def watch_glob(*patterns):
# Add Tiltfile reload watches for base dirs implied by patterns
seen = {}
for pat in patterns:
base = _base_dir_from_pattern(pat)
if base not in seen:
seen[base] = True
watch_file(base)


def files_glob(*glob_patterns, **kwargs):
"""
Expand one or more patterns into a list of real file paths using bash/find.
Supported patterns (good enough for most Tilt workflows):
- 'dir/**/*.ext' => find dir -type f -name "*.ext"
- 'dir/*.ext' => find dir -maxdepth 1 -type f -name "*.ext"
- '*.ext' => find . -maxdepth 1 -type f -name "*.ext"
- literal file/dir => included as-is
Returns a de-duplicated list of paths.
"""
# Ensure Tiltfile reloads when new files matching these patterns are added
watch_glob(*glob_patterns)
results = []
for pat in glob_patterns:
# Handle recursive patterns with '**/' anywhere in the string (e.g., 'dir/**/x', '**/*.go')
if '/**/' in pat:
parts = pat.split('/**/', 1)
base = parts[0] if parts[0] else '.'
tail = parts[1] if len(parts) > 1 else ''
if not tail or tail == '*':
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -print'"
else:
if '/' in tail:
# Fallback to -path for complex tails
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -path \"*" + tail + "\" -print'"
else:
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -name \"" + tail + "\" -print'"
# Handle patterns that START with '**/' (e.g., '**/*.go')
elif pat.startswith('**/'):
tail = pat[len('**/'):]
if not tail or tail == '*':
find_cmd = "bash --noprofile --norc -lc 'find . -type f -print'"
else:
if '/' in tail:
find_cmd = "bash --noprofile --norc -lc 'find . -type f -path \"*" + tail + "\" -print'"
else:
find_cmd = "bash --noprofile --norc -lc 'find . -type f -name \"" + tail + "\" -print'"
# Root-level wildcard like '*.go'
elif pat.startswith('*.') or (pat and '*' in pat and '/' not in pat):
find_cmd = "bash --noprofile --norc -lc 'find . -maxdepth 1 -type f -name \"" + pat + "\" -print'"
# Directory-limited wildcard like 'dir/*.templ'
elif '*' in pat and '/' in pat:
dir_part, name_pat = pat.rsplit('/', 1)
find_cmd = "bash --noprofile --norc -lc 'find " + dir_part + " -maxdepth 1 -type f -name \"" + name_pat + "\" -print'"
else:
# Literal path (file or dir). deps can watch directories too; include as-is.
results.append(pat)
continue

out = local(command=find_cmd, quiet=True)
s = str(out).strip()
if s:
for line in s.split('\n'):
p = line.strip()
if p.startswith('./'):
p = p[2:]
if p:
results.append(p)

# Deduplicate while preserving order
seen = {}
deduped = []
for p in results:
if p not in seen:
seen[p] = True
deduped.append(p)
return deduped

34 changes: 34 additions & 0 deletions files_glob/test/Tiltfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
load('../Tiltfile', 'files_glob')

# Doesn't use any kubernetes context, therefore safe
allow_k8s_contexts(k8s_context())

# Create some sample files for matching
# (These are committed to the repo; tests assume files exist on disk.)

# Validate root-level pattern
paths_root_txt = files_glob('*.txt')
expected_root_txt = ['root.txt']
if set(paths_root_txt) != set(expected_root_txt):
fail('root-level pattern failed: got %s, expected %s' % (paths_root_txt, expected_root_txt))

# Validate non-recursive dir pattern
paths_dir1_txt = files_glob('dir1/*.txt')
expected_dir1_txt = ['dir1/a.txt', 'dir1/b.txt']
if set(paths_dir1_txt) != set(expected_dir1_txt):
fail('dir1/*.txt pattern failed: got %s, expected %s' % (paths_dir1_txt, expected_dir1_txt))

# Validate recursive pattern
paths_dir2_txt = files_glob('dir2/**/*.txt')
expected_dir2_txt = ['dir2/nested/t1.txt', 'dir2/nested/t2.txt']
if set(paths_dir2_txt) != set(expected_dir2_txt):
fail('dir2/**/*.txt pattern failed: got %s, expected %s' % (paths_dir2_txt, expected_dir2_txt))

# Validate deduplication (same files via two overlapping patterns)
paths_dedupe = files_glob('dir1/*.txt', 'dir1/**/*.txt')
if len(paths_dedupe) != len(set(paths_dedupe)):
fail('deduplication failed: got %d items with duplicates: %s' % (len(paths_dedupe), paths_dedupe))

# CI expects at least one resource
local_resource('dummy', 'echo ok')

2 changes: 2 additions & 0 deletions files_glob/test/dir1/a.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alpha

2 changes: 2 additions & 0 deletions files_glob/test/dir1/b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bravo

2 changes: 2 additions & 0 deletions files_glob/test/dir2/nested/t1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
t1 text

2 changes: 2 additions & 0 deletions files_glob/test/dir2/nested/t2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
t2 text

1 change: 1 addition & 0 deletions files_glob/test/root.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root sample